Introduce code from Rust monorepo. This is a mess

This commit is contained in:
Brandon Thomas
2022-02-28 05:43:54 -05:00
parent 921fb49f9a
commit ba326bfcfd
1058 changed files with 192192 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp import data, losses, models, modules
from nemo.package_info import __version__
# Set collection version equal to NeMo version.
__version = __version__
# Authorship.
__author__ = "NVIDIA Corporation"
# Set collection name.
__description__ = "Natural Language Processing collection"
+40
View File
@@ -0,0 +1,40 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.data_utils import *
from nemo.collections.nlp.data.entity_linking.entity_linking_dataset import EntityLinkingDataset
from nemo.collections.nlp.data.information_retrieval.information_retrieval_dataset import (
BertInformationRetrievalDataset,
)
from nemo.collections.nlp.data.language_modeling.l2r_lm_dataset import (
L2RLanguageModelingDataset,
TarredL2RLanguageModelingDataset,
)
from nemo.collections.nlp.data.language_modeling.lm_bert_dataset import (
BertPretrainingDataset,
BertPretrainingPreprocessedDataloader,
)
from nemo.collections.nlp.data.language_modeling.sentence_dataset import SentenceDataset, TarredSentenceDataset
from nemo.collections.nlp.data.machine_translation.machine_translation_dataset import (
TarredTranslationDataset,
TranslationDataset,
)
from nemo.collections.nlp.data.neural_machine_translation.neural_machine_translation_dataset import (
NeuralMachineTranslationDataset,
)
from nemo.collections.nlp.data.question_answering_squad.qa_dataset import SquadDataset
from nemo.collections.nlp.data.token_classification.token_classification_dataset import (
BertTokenClassificationDataset,
BertTokenClassificationInferDataset,
)
@@ -0,0 +1,15 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.data_utils.data_preprocessing import *
@@ -0,0 +1,479 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import csv
import json
import os
import pickle
import random
import re
import string
from collections import Counter
import numpy as np
import torch
from tqdm.auto import tqdm
from nemo.utils import logging
from nemo.utils.env_var_parsing import get_envint
__all__ = [
'DataProcessor',
'get_label_stats',
'partition_data',
'write_files',
'write_data',
'create_dataset',
'read_csv',
'get_dataset',
'partition',
'map_entities',
'get_entities',
'get_data',
'reverse_dict',
'get_intent_labels',
'get_stats',
'DATABASE_EXISTS_TMP',
'MODE_EXISTS_TMP',
'is_whitespace',
'write_vocab',
'if_exist',
'remove_punctuation_from_sentence',
'dataset_to_ids',
'get_freq_weights',
'fill_class_weights',
'normalize_answer',
'get_labels_to_labels_id_mapping',
'get_vocab',
'find_newlines',
'load_data_indices',
]
DATABASE_EXISTS_TMP = '{} dataset has already been processed and stored at {}'
MODE_EXISTS_TMP = '{} mode of {} dataset has already been processed and stored at {}'
class DataProcessor(object):
"""Base class for data converters for sequence classification data sets."""
def get_train_examples(self, data_dir):
"""Gets a collection of `InputExample`s for the train set."""
raise NotImplementedError()
def get_dev_examples(self, data_dir):
"""Gets a collection of `InputExample`s for the dev set."""
raise NotImplementedError()
def get_labels(self):
"""Gets the list of labels for this data set."""
raise NotImplementedError()
@classmethod
def _read_tsv(cls, input_file, quotechar=None):
"""Reads a tab separated value file."""
with open(input_file, "r", encoding="utf-8-sig") as f:
reader = csv.reader(f, delimiter="\t", quotechar=quotechar)
lines = []
for line in reader:
# if sys.version_info[0] == 2:
# line = list(unicode(cell, 'utf-8') for cell in line)
lines.append(line)
return lines
def normalize_answer(s):
"""Lower text and remove punctuation, articles and extra whitespace."""
def remove_articles(text):
return re.sub(r'\b(a|an|the)\b', ' ', text)
def white_space_fix(text):
return ' '.join(text.split())
def remove_punc(text):
exclude = set(string.punctuation)
return ''.join(ch for ch in text if ch not in exclude)
def lower(text):
return text.lower()
return white_space_fix(remove_articles(remove_punc(lower(s))))
def get_label_stats(labels, outfile='stats.tsv', verbose=True):
'''
Args:
labels: list of all labels
outfile: path to the file where to save label stats
Returns:
total (int): total number of labels
label_frequencies (list of tuples): each tuple represent (label, label frequency)
max id of the labels
'''
labels = Counter(labels)
total = sum(labels.values())
out = open(outfile, 'w')
i = 0
freq_dict = {}
label_frequencies = labels.most_common()
for k, v in label_frequencies:
out.write(f'{k}\t\t{round(v/total,5)}\t\t{v}\n')
if verbose and i < 3:
logging.info(f'label: {k}, {v} out of {total} ({(v / total)*100.0:.2f}%).')
i += 1
freq_dict[k] = v
return total, freq_dict, max(labels.keys())
def partition_data(intent_queries, slot_tags, split=0.1):
n = len(intent_queries)
n_dev = int(n * split)
dev_idx = set(random.sample(range(n), n_dev))
dev_intents, dev_slots, train_intents, train_slots = [], [], [], []
dev_intents.append('sentence\tlabel\n')
train_intents.append('sentence\tlabel\n')
for i, item in enumerate(intent_queries):
if i in dev_idx:
dev_intents.append(item)
dev_slots.append(slot_tags[i])
else:
train_intents.append(item)
train_slots.append(slot_tags[i])
return train_intents, train_slots, dev_intents, dev_slots
def write_files(data, outfile):
with open(outfile, 'w') as f:
for item in data:
item = f'{item.strip()}\n'
f.write(item)
def write_data(data, slot_dict, intent_dict, outfold, mode, uncased):
intent_file = open(f'{outfold}/{mode}.tsv', 'w')
intent_file.write('sentence\tlabel\n')
slot_file = open(f'{outfold}/{mode}_slots.tsv', 'w')
for tokens, slots, intent in data:
text = ' '.join(tokens)
if uncased:
text = text.lower()
intent_file.write(f'{text}\t{intent_dict[intent]}\n')
slots = [str(slot_dict[slot]) for slot in slots]
slot_file.write(' '.join(slots) + '\n')
intent_file.close()
slot_file.close()
def create_dataset(train, dev, slots, intents, uncased, outfold):
os.makedirs(outfold, exist_ok=True)
if 'O' in slots:
slots.remove('O')
slots = sorted(list(slots)) + ['O']
intents = sorted(list(intents))
slots = write_vocab(slots, f'{outfold}/dict.slots.csv')
intents = write_vocab(intents, f'{outfold}/dict.intents.csv')
write_data(train, slots, intents, outfold, 'train', uncased)
write_data(dev, slots, intents, outfold, 'test', uncased)
def read_csv(file_path):
rows = []
with open(file_path, 'r') as csvfile:
read_csv = csv.reader(csvfile, delimiter=',')
for row in read_csv:
rows.append(row)
return rows
def get_dataset(files, dev_split=0.1):
# entity2value, value2entity = get_entities(files)
data, slots, intents = get_data(files)
if len(data) == 1:
train, dev = partition(data[0], split=dev_split)
else:
train, dev = data[0], data[1]
return train, dev, slots, intents
def partition(data, split=0.1):
n = len(data)
n_dev = int(n * split)
dev_idx = set(random.sample(range(n), n_dev))
dev, train = [], []
for i, item in enumerate(data):
if i in dev_idx:
dev.append(item)
else:
train.append(item)
return train, dev
def map_entities(entity2value, entities):
for key in entities:
if 'data' in entities[key]:
if key not in entity2value:
entity2value[key] = set([])
values = []
for value in entities[key]['data']:
values.append(value['value'])
values.extend(value['synonyms'])
entity2value[key] = entity2value[key] | set(values)
return entity2value
def get_entities(files):
entity2value = {}
for file in files:
with open(file, 'r') as json_file:
data = json.load(json_file)
entity2value = map_entities(entity2value, data['entities'])
value2entity = reverse_dict(entity2value)
return entity2value, value2entity
def get_data(files):
all_data, all_slots, all_intents = [], set(['O']), set()
for file in files:
file_data = []
with open(file, 'r') as json_file:
data = json.load(json_file)
for intent in data['intents']:
all_intents.add(intent)
utterances = data['intents'][intent]['utterances']
for utterance in utterances:
tokens, slots = [], []
for frag in utterance['data']:
frag_tokens = frag['text'].strip().split()
tokens.extend(frag_tokens)
if 'slot_name' not in frag:
slot = 'O'
else:
slot = frag['slot_name']
all_slots.add(slot)
slots.extend([slot] * len(frag_tokens))
file_data.append((tokens, slots, intent))
all_data.append(file_data)
return all_data, all_slots, all_intents
def reverse_dict(entity2value):
value2entity = {}
for entity in entity2value:
for value in entity2value[entity]:
value2entity[value] = entity
return value2entity
def get_intent_labels(intent_file):
labels = {}
label = 0
with open(intent_file, 'r') as f:
for line in f:
intent = line.strip()
labels[intent] = label
label += 1
return labels
def get_stats(lengths):
logging.info('Some stats of the lengths of the sequences:')
lengths = np.asarray(lengths)
logging.info(
f'Min: {np.min(lengths)} | \
Max: {np.max(lengths)} | \
Mean: {np.mean(lengths)} | \
Median: {np.median(lengths)}'
)
logging.info(f'75 percentile: {np.percentile(lengths, 75):.2f}')
logging.info(f'99 percentile: {np.percentile(lengths, 99):.2f}')
def is_whitespace(c):
if c == " " or c == "\t" or c == "\r" or c == "\n" or ord(c) == 0x202F:
return True
return False
def write_vocab(items, outfile):
vocab = {}
idx = 0
with open(outfile, 'w') as f:
for item in items:
f.write(item + '\n')
vocab[item] = idx
idx += 1
return vocab
def get_labels_to_labels_id_mapping(file):
'''
Reads labels from the file and returns labels to id mapping dictionary
Args:
file: path to file
Returns:
labels to id mapping dictionary
'''
lines = open(file, 'r').readlines()
lines = [line.strip() for line in lines if line.strip()]
label_ids = {lines[i]: i for i in range(len(lines))}
return label_ids
def if_exist(outfold, files):
if not os.path.exists(outfold):
return False
for file in files:
if not os.path.exists(f'{outfold}/{file}'):
return False
return True
def remove_punctuation_from_sentence(sentence):
sentence = re.sub('[' + string.punctuation + ']', '', sentence)
sentence = sentence.lower()
return sentence
def dataset_to_ids(dataset, tokenizer, cache_ids=False, add_bos_eos=True, cache_data_per_node=False, use_cache=False):
"""
Reads dataset from file line by line, tokenizes each line with tokenizer,
and returns list of lists which corresponds to ids of tokenized strings.
Args:
dataset (str): path to dataset
tokenizer: tokenizer to convert text into ids
cache_ids (bool): if True, ids are saved to disk as pickle file
with similar name (e.g., data.txt --> data.txt.pkl)
add_bos_eos (bool): whether to add <s> and </s> symbols (e.g., for NMT)
cache_data_per_node (bool): Cache data on local_rank 0. Use when there is not a shared-filesystem.
use_cache (bool): Use cached ids if they exist.
Returns:
ids: list of ids which correspond to tokenized strings of the dataset
"""
cached_ids_dataset = dataset + str(".pkl")
if use_cache and os.path.isfile(cached_ids_dataset):
logging.info("Loading cached tokenized dataset ...")
ids = pickle.load(open(cached_ids_dataset, "rb"))
else:
logging.info(f"Tokenizing dataset {dataset}...")
data = open(dataset, "rb").readlines()
ids = []
for sentence in tqdm(data, desc='Tokenizing sentence'):
sent_ids = tokenizer.text_to_ids(sentence.decode("utf-8"))
if add_bos_eos:
sent_ids = [tokenizer.bos_id] + sent_ids + [tokenizer.eos_id]
ids.append(sent_ids)
if cache_ids and (
not torch.distributed.is_initialized() or (cache_data_per_node and get_envint("LOCAL_RANK", 0) == 0)
):
logging.info("Caching tokenized dataset ...")
pickle.dump(ids, open(cached_ids_dataset, "wb"))
return ids
def get_freq_weights(label_freq):
"""
Goal is to give more weight to the classes with less samples
so as to match the ones with the higher frequencies. We achieve this by
dividing the total frequency by the freq of each label to calculate its weight.
"""
total_size = 0
for lf in label_freq.values():
total_size += lf
weighted_slots = {label: (total_size / (len(label_freq) * freq)) for label, freq in label_freq.items()}
return weighted_slots
def fill_class_weights(weights, max_id=-1):
"""
Gets a dictionary of labels with their weights and creates a list with size of the labels filled with those weights.
Missing labels in the dictionary would get value 1.
Args:
weights: dictionary of weights for labels, labels as keys and weights are their values
max_id: the largest label id in the dataset, default=-1 would consider the largest label in the weights dictionary as max_id
Returns:
weights_list: list of weights for labels
"""
if max_id < 0:
max_id = 0
for l in weights.keys():
max_id = max(max_id, l)
all_weights = [1.0] * (max_id + 1)
for i in range(len(all_weights)):
if i in weights:
all_weights[i] = weights[i]
return all_weights
def get_vocab(file):
lines = open(file, 'r').readlines()
lines = [line.strip() for line in lines if line.strip()]
labels = {i: lines[i] for i in range(len(lines))}
return labels
def find_newlines(contents):
"""
Finds all of the newline positions in a text file.
"""
start = 0
while True:
try:
# index and split are much faster than Python for loops
new_start = contents.index(b"\n", start)
line = (
contents[start:new_start]
.replace(b"\xc2\x99", b" ")
.replace(b"\xc2\xa0", b" ")
.decode("utf-8", errors="ignore")
)
if len(line.split()) > 0:
yield start
start = new_start + 1
except ValueError:
break
def load_data_indices(idx_file: str, data_file: str, savename: str):
"""
Loads dataset index file if it exsits
"""
data_dir = data_file[: data_file.rfind('/')]
mode = data_file[data_file.rfind('/') + 1 : data_file.rfind('.')]
idx_file = f"{data_dir}/{mode}_{savename}.pkl"
if os.path.isfile(idx_file):
# If the sentence indices file already exists, load from it
with open(idx_file, "rb") as f:
indices = pickle.load(f)
return indices, idx_file, data_dir
return None, idx_file, data_dir
@@ -0,0 +1,17 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.data_processor import SGDDataProcessor
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.dataset import SGDDataset
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.schema import Schema
@@ -0,0 +1,13 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,676 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/baseline/data_utils.py
"""
import collections
import json
import os
import pickle
import re
from typing import Dict, List
import numpy as np
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.input_example import InputExample
from nemo.utils import logging
__all__ = ['SGDDataProcessor']
FILE_RANGES = {
"sgd_single_domain": {"train": range(1, 44), "dev": range(1, 8), "test": range(1, 12)},
"sgd_multi_domain": {"train": range(44, 128), "dev": range(8, 21), "test": range(12, 35)},
"sgd_all": {"train": range(1, 128), "dev": range(1, 21), "test": range(1, 35)},
"sgd_all_single": {"train": range(1, 128), "dev": range(1, 8), "test": range(1, 12)},
"multiwoz": {"train": range(1, 18), "dev": range(1, 3), "test": range(1, 3)},
"debug_sample": {"train": range(1, 2), "dev": range(1, 2), "test": range(1, 2)},
}
class SGDDataProcessor(object):
"""Data generator for SGD dialogues."""
def __init__(
self,
task_name: str,
data_dir: str,
dialogues_example_dir: str,
tokenizer: object,
schemas: object,
schema_config: Dict[str, int],
subsample: bool = False,
):
"""
Constructs SGDDataProcessor
Args:
task_name: task name, e.g. "sgd_single_domain"
data_dir: path to data directory
dialogues_example_dir: path to store processed dialogue examples
tokenizer: tokenizer object
schemas: schema object
schema_config: schema configuration
subsample: whether to balance positive and negative samples in dataset
"""
self.data_dir = data_dir
self._task_name = task_name
self.schemas = schemas
self.schema_config = schema_config
train_file_range = FILE_RANGES[task_name]["train"]
dev_file_range = FILE_RANGES[task_name]["dev"]
test_file_range = FILE_RANGES[task_name]["test"]
self._file_ranges = {
"train": train_file_range,
"dev": dev_file_range,
"test": test_file_range,
}
self._seen_services = {
"train": set(),
"dev": set(),
"test": set(),
}
self._tokenizer = tokenizer
self._subsample = subsample
self._dialogues_example_dir = dialogues_example_dir
self.dial_files = {}
# slots_relation_list.np would contain the candidate list of slots for each (service, slot) which would be
# looked into when a switch between two services happens in the dialogue and we can not find any value for a slot in the current user utterance.
# This file would get generated from the dialogues in the training set.
self.slots_relation_file = os.path.join(dialogues_example_dir, f"{task_name}_train_slots_relation_list.np")
for dataset in ["train", "dev", "test"]:
# Process dialogue files
dial_file = f"{task_name}_{dataset}_examples.processed"
dial_file = os.path.join(dialogues_example_dir, dial_file)
self.dial_files[(task_name, dataset)] = dial_file
dialog_paths = SGDDataProcessor.get_dialogue_files(data_dir, dataset, task_name)
dialogs = SGDDataProcessor.load_dialogues(dialog_paths)
for dialog in dialogs:
self._seen_services[dataset].update(set(dialog['services']))
def save_dialog_examples(self, overwrite_dial_files: bool):
"""
Preprocesses dialogues and saves to disk.
Args:
overwrite_dial_files: whether or not to overwrite saved file if already exists
"""
for dataset in ["train", "dev", "test"]:
dial_file = self.dial_files[(self._task_name, dataset)]
if not os.path.exists(dial_file) or overwrite_dial_files:
logging.info(f"Start generating the dialogue examples for {dataset} dataset.")
if not os.path.exists(self._dialogues_example_dir):
os.makedirs(self._dialogues_example_dir)
dial_examples, slots_relation_list = self._generate_dialog_examples(
dataset, self.schemas, self._subsample
)
for ex in dial_examples:
del ex.service_schema
del ex._tokenizer
del ex.schema_config
del ex.user_utterance
del ex.categorical_slot_id
del ex.system_utterance
del ex.noncategorical_slot_id
del ex.categorical_slot_value_id
del ex.requested_slot_id
del ex.intent_id
with open(dial_file, "wb") as f:
np.save(f, dial_examples)
if dataset == "train":
with open(self.slots_relation_file, "wb") as f:
pickle.dump(slots_relation_list, f)
logging.info(f"The slot carry-over list for train set is stored at {self.slots_relation_file}")
logging.info(f"The dialogue examples for {dataset} dataset saved at {dial_file}")
logging.info(f"Finish generating the dialogue examples for {dataset} dataset.")
def get_dialog_examples(self, dataset_split: str) -> List[object]:
"""
Loads preprocessed dialogue examples from disk.
Args:
dataset_split: dataset split
Returns:
dial_examples: list of InputExample's.
"""
if (self._task_name, dataset_split) not in self.dial_files or not os.path.exists(
self.dial_files[(self._task_name, dataset_split)]
):
raise ValueError(
f"{dataset_split} dialogue examples were not processed for {self._task_name} task. Re-initialize SGDDataProcessor and add {dataset_split} dataset split to datasets arg."
)
dial_file = self.dial_files[(self._task_name, dataset_split)]
logging.info(f"Loading dialogue examples from {dial_file}.")
with open(dial_file, "rb") as f:
dial_examples = np.load(f, allow_pickle=True)
f.close()
if not os.path.exists(self.slots_relation_file):
raise ValueError(
f"Slots relation file {self.slots_relation_file} does not exist. It is needed for the carry-over mechanism of state tracker for switches between services."
)
if os.path.getsize(self.slots_relation_file) > 0:
with open(self.slots_relation_file, "rb") as f:
self.schemas._slots_relation_list = pickle.load(f)
logging.info(
f"Loaded the slot relation list for value carry-over between services from {self.slots_relation_file}."
)
return dial_examples
def get_seen_services(self, dataset_split: str):
"""
Returns list of seen services, i.e. both in given and training split
Args:
dataset_split: data split
Returns:
seen_services: list of seen services
"""
seen_services = self._seen_services[dataset_split]
return seen_services
def _generate_dialog_examples(self, dataset_split: str, schemas: object, subsample: bool):
"""
Returns a list of `InputExample`s of the data splits' dialogues.
Args:
dataset_split: data split, can be "train", "dev", or "test".
schemas: schema for all services of all datasets
subsample: whether to balance postive and negative samples in the dataset
Returns:
examples: a list of `InputExample`s.
"""
logging.info(f'Creating examples and slot relation list from the dialogues started...')
dialog_paths = [
os.path.join(self.data_dir, dataset_split, "dialogues_{:03d}.json".format(i))
for i in self._file_ranges[dataset_split]
]
dialogs = SGDDataProcessor.load_dialogues(dialog_paths)
examples = []
slot_carryover_candlist = collections.defaultdict(int)
for dialog_idx, dialog in enumerate(dialogs):
if dialog_idx % 1000 == 0:
logging.info(f'Processed {dialog_idx} dialogues.')
examples.extend(
self._create_examples_from_dialog(dialog, schemas, dataset_split, slot_carryover_candlist, subsample)
)
slots_relation_list = collections.defaultdict(list)
for slots_relation, relation_size in slot_carryover_candlist.items():
if relation_size > 0:
slots_relation_list[(slots_relation[0], slots_relation[1])].append(
(slots_relation[2], slots_relation[3], relation_size)
)
slots_relation_list[(slots_relation[2], slots_relation[3])].append(
(slots_relation[0], slots_relation[1], relation_size)
)
return examples, slots_relation_list
def _create_examples_from_dialog(
self, dialog: dict, schemas: object, dataset_split: str, slot_carryover_candlist: dict, subsample: bool
):
"""
Create examples for every turn in the dialogue.
Args:
dialog: dialogue example
schemas: schema for all services of all datasets
dataset_split: data split
slot_carryover_candlist: a dictionary to keep and count the number of carry-over cases between two slots from two different services
subsample: whether to balance postive and negative samples in the dataset
Returns:
examples: a list of `InputExample`s.
"""
dialog_id = dialog["dialogue_id"]
prev_states = {}
examples = []
for turn_idx, turn in enumerate(dialog["turns"]):
# Generate an example for every frame in every user turn.
if turn["speaker"] == "USER":
user_utterance = turn["utterance"]
user_frames = {f["service"]: f for f in turn["frames"]}
if turn_idx > 0:
system_turn = dialog["turns"][turn_idx - 1]
system_utterance = system_turn["utterance"]
system_frames = {f["service"]: f for f in system_turn["frames"]}
else:
system_utterance = ""
system_frames = {}
turn_id = "{}-{}-{:02d}".format(dataset_split, dialog_id, turn_idx)
turn_examples, prev_states, slot_carryover_values = self._create_examples_from_turn(
turn_id,
system_utterance,
user_utterance,
system_frames,
user_frames,
prev_states,
schemas,
subsample,
)
examples.extend(turn_examples)
for value, slots_list in slot_carryover_values.items():
if value in ["True", "False"]:
continue
if len(slots_list) > 1:
for service1, slot1 in slots_list:
for service2, slot2 in slots_list:
if service1 == service2:
continue
if service1 > service2:
service1, service2 = service2, service1
slot1, slot2 = slot2, slot1
slot_carryover_candlist[(service1, slot1, service2, slot2)] += 1
return examples
def _get_state_update(self, current_state: dict, prev_state: dict) -> dict:
"""
Updates dialogue state
Args:
current_state: slot values pairs for the current dialogue turn
prev_state: slot values pairs for the previous dialogue turns
Returns:
state_update: slot values pairs that are added/updated during the current dialogue turn
"""
state_update = dict(current_state)
for slot, values in current_state.items():
if slot in prev_state and prev_state[slot][0] in values:
# Remove the slot from state if its value didn't change.
state_update.pop(slot)
return state_update
def _create_examples_from_turn(
self,
turn_id: int,
system_utterance: str,
user_utterance: str,
system_frames: dict,
user_frames: dict,
prev_states: dict,
schemas: object,
subsample: bool,
):
"""
Creates an example for each frame in the user turn.
Args:
turn_id: turn number
system_utterance: last system utterance
user_utterance: lst user utterance
system_frames: all system utterances and slot - slot value pairs
user_frames: all user utterances and slot - slot value pairs
prev_states: slot - slot value pairs from the previous turns
schemas: schema for all services of all datasets
subsample: whether to balance postive and negative samples in the dataset
Returns:
examples: a list of `InputExample`s.
prev_states: updated dialogue state e.g. {'Restaurants_1': {'city': ['San Jose'], 'cuisine': ['American']}}
"""
user_tokens, user_alignments, user_inv_alignments = self._tokenize(user_utterance)
system_tokens, system_alignments, system_inv_alignments = self._tokenize(system_utterance)
system_user_utterance = system_utterance + ' ' + user_utterance
system_user_tokens, system_user_alignments, system_user_inv_alignments = self._tokenize(system_user_utterance)
states = {}
examples = []
slot_carryover_values = collections.defaultdict(list)
for service, user_frame in user_frames.items():
base_example = InputExample(schema_config=self.schema_config, tokenizer=self._tokenizer,)
base_example.service_schema = schemas.get_service_schema(service)
base_example.service_id = schemas.get_service_schema(service).service_id
system_frame = system_frames.get(service, None)
state = user_frame["state"]["slot_values"]
state_update = self._get_state_update(state, prev_states.get(service, {}))
states[service] = state
dataset_split, dialog_id, turn_id_ = turn_id.split('-')
dialog_id_1, dialog_id_2 = dialog_id.split('_')
base_example.example_id = f"{turn_id}-{service}"
base_example.example_id_num = [
int(dialog_id_1),
int(dialog_id_2),
int(turn_id_),
schemas.get_service_id(service),
]
for model_task in range(self.schema_config["NUM_TASKS"]):
if model_task == 0:
for intent_id, intent in enumerate(schemas.get_service_schema(service).intents):
task_example = base_example.make_copy()
task_example.task_mask[model_task] = 1
task_example.intent_id = intent_id
task_example.example_id += f"-{model_task}-{intent_id}-0"
task_example.example_id_num.extend([model_task, intent_id, 0])
intent_description = (
intent + " " + schemas.get_service_schema(service).intent_descriptions[intent]
)
intent_tokens, intent_alignments, intent_inv_alignments = self._tokenize(intent_description)
task_example.add_utterance_features(
intent_tokens,
intent_inv_alignments,
system_user_tokens,
system_user_inv_alignments,
intent_description,
system_user_utterance,
)
task_example.add_intents(user_frame)
examples.append(task_example)
if model_task == 1:
for slot_id, slot in enumerate(schemas.get_service_schema(service).slots):
task_example = base_example.make_copy()
task_example.task_mask[model_task] = 1
task_example.requested_slot_id = slot_id
task_example.example_id += f"-{model_task}-{slot_id}-0"
task_example.example_id_num.extend([model_task, slot_id, 0])
slot_description = slot + " " + schemas.get_service_schema(service).slot_descriptions[slot]
slot_tokens, slot_alignments, slot_inv_alignments = self._tokenize(slot_description)
task_example.add_utterance_features(
slot_tokens,
slot_inv_alignments,
user_tokens,
user_inv_alignments,
slot_description,
user_utterance,
)
task_example.add_requested_slots(user_frame)
examples.append(task_example)
if model_task == 2:
off_slots = []
on_slots = []
for slot_id, slot in enumerate(schemas.get_service_schema(service).categorical_slots):
task_example = base_example.make_copy()
task_example.task_mask[model_task] = 1
# assert task_example.task_mask == [0, 0, 1, 0, 0, 0]
task_example.categorical_slot_id = slot_id
task_example.example_id += f"-{model_task}-{slot_id}-0"
task_example.example_id_num.extend([model_task, slot_id, 0])
slot_description = slot + " " + schemas.get_service_schema(service).slot_descriptions[slot]
slot_tokens, slot_alignments, slot_inv_alignments = self._tokenize(slot_description)
task_example.add_utterance_features(
slot_tokens,
slot_inv_alignments,
system_user_tokens,
system_user_inv_alignments,
slot_description,
system_user_utterance,
)
task_example.add_categorical_slots(state_update)
if task_example.categorical_slot_status == 0:
off_slots.append(task_example)
else:
on_slots.append(task_example)
examples.append(task_example)
old_example = task_example
for value_id, value in enumerate(
schemas.get_service_schema(service).get_categorical_slot_values(slot)
):
if dataset_split != 'train' or task_example.categorical_slot_status == 1:
task_example = old_example.make_copy_of_categorical_features()
task_example.task_mask[3] = 1
# assert task_example.task_mask == [0, 0, 0, 1, 0, 0]
task_example.categorical_slot_id = slot_id
task_example.categorical_slot_value_id = value_id
task_example.example_id = base_example.example_id + f"-3-{slot_id}-{value_id}"
task_example.example_id_num = base_example.example_id_num + [3, slot_id, value_id]
slot_description = slot + " " + value # add slot description
slot_tokens, slot_alignments, slot_inv_alignments = self._tokenize(slot_description)
task_example.add_utterance_features(
slot_tokens,
slot_inv_alignments,
system_user_tokens,
system_user_inv_alignments,
slot_description,
system_user_utterance,
)
task_example.add_categorical_slots(state_update)
assert task_example.categorical_slot_status == old_example.categorical_slot_status
examples.append(task_example)
if dataset_split == 'train' and subsample:
num_on_slots = len(on_slots)
examples.extend(
np.random.choice(off_slots, replace=False, size=min(max(num_on_slots, 1), len(off_slots)))
)
else:
examples.extend(off_slots)
if model_task == 4: # noncat slot status
off_slots = []
on_slots = []
for slot_id, slot in enumerate(schemas.get_service_schema(service).non_categorical_slots):
task_example = base_example.make_copy()
task_example.task_mask[model_task] = 1
# assert task_example.task_mask == [0, 0, 0, 0, 1, 0]
task_example.noncategorical_slot_id = slot_id
task_example.example_id += f"-{model_task}-{slot_id}-0"
task_example.example_id_num.extend([model_task, slot_id, 0])
slot_description = slot + " " + schemas.get_service_schema(service).slot_descriptions[slot]
slot_tokens, slot_alignments, slot_inv_alignments = self._tokenize(slot_description)
task_example.add_utterance_features(
slot_tokens,
slot_inv_alignments,
system_user_tokens,
system_user_inv_alignments,
slot_description,
system_user_utterance,
)
user_span_boundaries = self._find_subword_indices(
state_update,
user_utterance,
user_frame["slots"],
user_alignments,
user_tokens,
2 + len(slot_tokens) + len(system_tokens),
)
if system_frame is not None:
system_span_boundaries = self._find_subword_indices(
state_update,
system_utterance,
system_frame["slots"],
system_alignments,
system_tokens,
2 + len(slot_tokens),
)
else:
system_span_boundaries = {}
task_example.add_noncategorical_slots(
state_update, user_span_boundaries, system_span_boundaries
)
if task_example.noncategorical_slot_status == 0:
off_slots.append(task_example)
else:
on_slots.append(task_example)
examples.append(task_example)
if dataset_split != 'train' or task_example.noncategorical_slot_status == 1:
task_example = task_example.make_copy_of_non_categorical_features()
task_example.task_mask[5] = 1
# assert task_example.task_mask == [0, 0, 0, 0, 0, 1]
task_example.example_id = base_example.example_id + f"-5-{slot_id}-0"
task_example.example_id_num = base_example.example_id_num + [5, slot_id, 0]
examples.append(task_example)
if dataset_split == 'train' and subsample:
num_on_slots = len(on_slots)
examples.extend(
np.random.choice(off_slots, replace=False, size=min(max(num_on_slots, 1), len(off_slots)))
)
else:
examples.extend(off_slots)
if service not in prev_states and int(turn_id_) > 0:
for slot_name, values in state_update.items():
for value in values:
slot_carryover_values[value].append((service, slot_name))
for prev_service, prev_slot_value_list in prev_states.items():
if prev_service == service:
continue
if prev_service in state:
prev_slot_value_list = state[prev_service]
for prev_slot_name, prev_values in prev_slot_value_list.items():
for prev_value in prev_values:
slot_carryover_values[prev_value].append((prev_service, prev_slot_name))
return examples, states, slot_carryover_values
def _find_subword_indices(
self,
slot_values: dict,
utterance: str,
char_slot_spans: dict,
alignments: List[int],
subwords: List[str],
bias: int,
) -> dict:
"""
Find indices for subwords corresponding to slot values.
Args:
slot_values: slot - slot value pairs
utterance: utterance
char_slot_spans: char - slot spans
alignments: alignments
subwords: subtokens mapping
bias: offset
Returns:
span_boundaries: span boundaries
"""
span_boundaries = {}
for slot, values in slot_values.items():
# Get all values present in the utterance for the specified slot.
value_char_spans = {}
for slot_span in char_slot_spans:
if slot_span["slot"] == slot:
value = utterance[slot_span["start"] : slot_span["exclusive_end"]]
start_tok_idx = alignments[slot_span["start"]]
end_tok_idx = alignments[slot_span["exclusive_end"] - 1]
if 0 <= start_tok_idx < len(subwords):
end_tok_idx = min(end_tok_idx, len(subwords) - 1)
value_char_spans[value] = (start_tok_idx + bias, end_tok_idx + bias)
for v in values:
if v in value_char_spans:
span_boundaries[slot] = value_char_spans[v]
break
return span_boundaries
def _tokenize(self, utterance: str):
"""
Tokenize the utterance
Args:
utterance: A string containing the utterance to be tokenized.
Returns:
bert_tokens: A list of tokens obtained by word-piece tokenization of the
utterance.
alignments: A dict mapping indices of characters corresponding to start
and end positions of words (not subwords) to corresponding indices in
bert_tokens list.
inverse_alignments: A list of size equal to bert_tokens. Each element is a
tuple containing the index of the starting and inclusive ending
character of the word corresponding to the subword. This list is used
during inference to map word-piece indices to spans in the original
utterance.
"""
# utterance = tokenization.convert_to_unicode(utterance)
# After _naive_tokenize, spaces and punctuation marks are all retained, i.e.
# direct concatenation of all the tokens in the sequence will be the
# original string.
tokens = SGDDataProcessor._naive_tokenize(utterance)
# ['I', ' ', 'am', ' ', 'feeling', ' ', 'hungry', ' ', 'so', ' ', 'I', ' ', 'would', ' ', 'like', ' ', 'to', ' ', 'find', ' ', 'a', ' ', 'place', ' ', 'to', ' ', 'eat', '.']
# Filter out empty tokens and obtain aligned character index for each token.
alignments = {}
char_index = 0
bert_tokens = (
[]
) # ['I', 'am', 'feeling', 'hungry', 'so', 'I', 'would', 'like', 'to', 'find', 'a', 'place', 'to', 'eat', '.']
# These lists store inverse alignments to be used during inference.
bert_tokens_start_chars = []
bert_tokens_end_chars = []
for token in tokens:
if token.strip():
subwords = self._tokenizer.text_to_tokens(token)
# Store the alignment for the index of starting character and the
# inclusive ending character of the token.
alignments[char_index] = len(bert_tokens)
bert_tokens_start_chars.extend([char_index] * len(subwords))
bert_tokens.extend(subwords)
# The inclusive ending character index corresponding to the word.
inclusive_char_end = char_index + len(token) - 1
alignments[inclusive_char_end] = len(bert_tokens) - 1
bert_tokens_end_chars.extend([inclusive_char_end] * len(subwords))
char_index += len(token)
inverse_alignments = list(zip(bert_tokens_start_chars, bert_tokens_end_chars))
return bert_tokens, alignments, inverse_alignments
@classmethod
def _naive_tokenize(cls, s: str):
"""
Tokenizes a string, separating words, spaces and punctuations.
Args:
s: a string
Returns:
seq_tok: list of words, spaces and punctuations from the string
"""
# Spaces and punctuation marks are all retained, i.e. direct concatenation
# of all the tokens in the sequence will be the original string.
seq_tok = [tok for tok in re.split(r"([^a-zA-Z0-9])", s) if tok]
return seq_tok
@classmethod
def load_dialogues(cls, dialog_json_filepaths: List[str]) -> List[dict]:
"""
Obtain the list of all dialogues from specified json files.
Args:
dialog_json_filepaths: list of json files
Returns:
dialogs: the list of all dialogues
"""
dialogs = []
for dialog_json_filepath in sorted(dialog_json_filepaths):
with open(dialog_json_filepath, 'r') as f:
dialogs.extend(json.load(f))
f.close()
return dialogs
@classmethod
def get_dialogue_files(cls, data_dir: str, dataset_split: str, task_name: str):
"""
Obtain the list of all dialogue json files
Args:
data_dir: path to the data folder
dataset_split: data split
task_name: SGD task name, see keys of the FILE_RANGES
Returns:
dialog: the list of all dialogue json files paths
"""
return [
os.path.join(data_dir, dataset_split, 'dialogues_{:03d}.json'.format(fid))
for fid in FILE_RANGES[task_name][dataset_split]
]
@@ -0,0 +1,85 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst
"""
from typing import Dict, Optional
import numpy as np
from nemo.core.classes import Dataset
from nemo.core.neural_types import ChannelType, LabelsType, NeuralType
__all__ = ['SGDDataset']
class SGDDataset(Dataset):
"""Processes SGD dataset"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports."""
return {
"example_id_num": NeuralType(('B', 'T'), ChannelType()),
"service_id": NeuralType(('B'), ChannelType()),
"utterance_ids": NeuralType(('B', 'T'), ChannelType()),
"token_type_ids": NeuralType(('B', 'T'), ChannelType()), # utterance segment
"attention_mask": NeuralType(('B', 'T'), ChannelType()), # utterance mask
"intent_status": NeuralType(('B'), LabelsType()),
"requested_slot_status": NeuralType(('B'), LabelsType()),
"categorical_slot_status": NeuralType(('B'), LabelsType()),
"categorical_slot_value_status": NeuralType(('B'), LabelsType()),
"noncategorical_slot_status": NeuralType(('B'), LabelsType()),
"noncategorical_slot_value_start": NeuralType(('B'), LabelsType()),
"noncategorical_slot_value_end": NeuralType(('B'), LabelsType()),
"start_char_idx": NeuralType(('B', 'T'), LabelsType()),
"end_char_idx": NeuralType(('B', 'T'), LabelsType()),
"task_mask": NeuralType(('B', 'T'), ChannelType()),
}
def __init__(self, dataset_split: str, dialogues_processor: object):
""" Constructor
Args:
dataset_split: dataset split
dialogues_processor: Data generator for SGD dialogues
"""
self.features = dialogues_processor.get_dialog_examples(dataset_split)
def __len__(self):
return len(self.features)
def __getitem__(self, idx: int):
ex = self.features[idx]
service_id = ex.service_id
return (
np.array(ex.example_id_num),
np.array(service_id),
np.array(ex.utterance_ids),
np.array(ex.utterance_segment),
np.array(ex.utterance_mask, dtype=np.long),
np.array(ex.intent_status, dtype=np.float32),
np.array(ex.requested_slot_status, dtype=np.float32),
np.array(ex.categorical_slot_status),
np.array(ex.categorical_slot_value_status, dtype=np.float32),
np.array(ex.noncategorical_slot_status),
np.array(ex.noncategorical_slot_value_start),
np.array(ex.noncategorical_slot_value_end),
np.array(ex.start_char_idx), # noncat_alignment_start
np.array(ex.end_char_idx), # noncat_alignment_end
np.array(ex.task_mask), # noncat_alignment_end
)
@@ -0,0 +1,294 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Evaluate predictions JSON file, w.r.t. ground truth file.
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/evaluate.py
"""
import collections
import glob
import json
import os
import numpy as np
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.metrics import (
ACTIVE_INTENT_ACCURACY,
JOINT_CAT_ACCURACY,
JOINT_GOAL_ACCURACY,
JOINT_NONCAT_ACCURACY,
NAN_VAL,
REQUESTED_SLOTS_F1,
REQUESTED_SLOTS_PRECISION,
REQUESTED_SLOTS_RECALL,
SLOT_TAGGING_F1,
SLOT_TAGGING_PRECISION,
SLOT_TAGGING_RECALL,
get_active_intent_accuracy,
get_average_and_joint_goal_accuracy,
get_requested_slots_f1,
get_slot_tagging_f1,
)
from nemo.utils import logging
__all__ = ['get_in_domain_services']
ALL_SERVICES = "#ALL_SERVICES"
SEEN_SERVICES = "#SEEN_SERVICES"
UNSEEN_SERVICES = "#UNSEEN_SERVICES"
# Name of the file containing all predictions and their corresponding frame metrics.
PER_FRAME_OUTPUT_FILENAME = "dialogues_and_metrics.json"
def get_service_set(schema_path: str) -> set:
"""
Get the set of all services present in a schema.
Args:
schema_path: schema file path
Returns:
service_set: set of services in file
"""
service_set = set()
with open(schema_path) as f:
schema = json.load(f)
for service in schema:
service_set.add(service["service_name"])
f.close()
return service_set
def get_in_domain_services(schema_path: str, service_set: set) -> set:
"""Get the set of common services between a schema and set of services.
Args:
schema_path: path to schema file
service_set: set of services
Returns:
joint_services: joint services between schema path file and service set
"""
joint_services = get_service_set(schema_path) & service_set
return joint_services
def get_dataset_as_dict(file_path_patterns) -> dict:
"""Read the DSTC8/SGD json dialogue data as dictionary with dialog ID as keys.
Args:
file_path_patterns: list or directory of files
Returns:
dataset_dict: dataset dictionary with dialog ID as keys
"""
dataset_dict = {}
if isinstance(file_path_patterns, list):
list_fp = file_path_patterns
else:
list_fp = sorted(glob.glob(file_path_patterns))
for fp in list_fp:
if PER_FRAME_OUTPUT_FILENAME in fp:
continue
logging.debug("Loading file: %s", fp)
with open(fp) as f:
data = json.load(f)
if isinstance(data, list):
for dial in data:
dataset_dict[dial["dialogue_id"]] = dial
elif isinstance(data, dict):
dataset_dict.update(data)
f.close()
return dataset_dict
def get_metrics(
dataset_ref: dict,
dataset_hyp: dict,
service_schemas: dict,
in_domain_services: set,
joint_acc_across_turn: bool,
use_fuzzy_match: bool,
):
"""Calculate the DSTC8/SGD metrics.
Args:
dataset_ref: The ground truth dataset represented as a dict mapping dialogue id to the corresponding dialogue.
dataset_hyp: The predictions in the same format as `dataset_ref`.
service_schemas: A dict mapping service name to the schema for the service.
in_domain_services: The set of services which are present in the training set.
joint_acc_across_turn: Whether to compute joint accuracy across turn instead of across service. Should be set to True when conducting multiwoz style evaluation.
use_fuzzy_match: Whether to use fuzzy string matching when comparing non-categorical slot values. Should be set to False when conducting multiwoz style evaluation.
Returns:
all_metric_aggregate: A dict mapping a metric collection name to a dict containing the values
for various metrics. Each metric collection aggregates the metrics across a specific set of frames in the dialogues.
per_frame_metric: metrics aggregated for each frame
"""
# Metrics can be aggregated in various ways, eg over all dialogues, only for
# dialogues containing unseen services or for dialogues corresponding to a
# single service. This aggregation is done through metric_collections, which
# is a dict mapping a collection name to a dict, which maps a metric to a list
# of values for that metric. Each value in this list is the value taken by
# the metric on a frame.
metric_collections = collections.defaultdict(lambda: collections.defaultdict(list))
# Ensure the dialogs in dataset_hyp also occur in dataset_ref.
assert set(dataset_hyp.keys()).issubset(set(dataset_ref.keys()))
logging.debug("len(dataset_hyp)=%d, len(dataset_ref)=%d", len(dataset_hyp), len(dataset_ref))
# Store metrics for every frame for debugging.
per_frame_metric = {}
for dial_id, dial_hyp in dataset_hyp.items():
dial_ref = dataset_ref[dial_id]
if set(dial_ref["services"]) != set(dial_hyp["services"]):
raise ValueError(
"Set of services present in ground truth and predictions don't match "
"for dialogue with id {}".format(dial_id)
)
joint_metrics = [JOINT_GOAL_ACCURACY, JOINT_CAT_ACCURACY, JOINT_NONCAT_ACCURACY]
for turn_id, (turn_ref, turn_hyp) in enumerate(zip(dial_ref["turns"], dial_hyp["turns"])):
metric_collections_per_turn = collections.defaultdict(lambda: collections.defaultdict(lambda: 1.0))
if turn_ref["speaker"] != turn_hyp["speaker"]:
raise ValueError("Speakers don't match in dialogue with id {}".format(dial_id))
# Skip system turns because metrics are only computed for user turns.
if turn_ref["speaker"] != "USER":
continue
if turn_ref["utterance"] != turn_hyp["utterance"]:
logging.error("Ref utt: %s", turn_ref["utterance"])
logging.error("Hyp utt: %s", turn_hyp["utterance"])
raise ValueError("Utterances don't match for dialogue with id {}".format(dial_id))
hyp_frames_by_service = {frame["service"]: frame for frame in turn_hyp["frames"]}
# Calculate metrics for each frame in each user turn.
for frame_ref in turn_ref["frames"]:
service_name = frame_ref["service"]
if service_name not in hyp_frames_by_service:
raise ValueError(
"Frame for service {} not found in dialogue with id {}".format(service_name, dial_id)
)
service = service_schemas[service_name]
frame_hyp = hyp_frames_by_service[service_name]
active_intent_acc = get_active_intent_accuracy(frame_ref, frame_hyp)
slot_tagging_f1_scores = get_slot_tagging_f1(frame_ref, frame_hyp, turn_ref["utterance"], service)
requested_slots_f1_scores = get_requested_slots_f1(frame_ref, frame_hyp)
goal_accuracy_dict = get_average_and_joint_goal_accuracy(
frame_ref, frame_hyp, service, use_fuzzy_match
)
frame_metric = {
ACTIVE_INTENT_ACCURACY: active_intent_acc,
REQUESTED_SLOTS_F1: requested_slots_f1_scores.f1,
REQUESTED_SLOTS_PRECISION: requested_slots_f1_scores.precision,
REQUESTED_SLOTS_RECALL: requested_slots_f1_scores.recall,
}
if slot_tagging_f1_scores is not None:
frame_metric[SLOT_TAGGING_F1] = slot_tagging_f1_scores.f1
frame_metric[SLOT_TAGGING_PRECISION] = slot_tagging_f1_scores.precision
frame_metric[SLOT_TAGGING_RECALL] = slot_tagging_f1_scores.recall
frame_metric.update(goal_accuracy_dict)
frame_id = "{:s}-{:03d}-{:s}".format(dial_id, turn_id, frame_hyp["service"])
per_frame_metric[frame_id] = frame_metric
# Add the frame-level metric result back to dialogues.
frame_hyp["metrics"] = frame_metric
# Get the domain name of the service.
domain_name = frame_hyp["service"].split("_")[0]
domain_keys = [ALL_SERVICES, frame_hyp["service"], domain_name]
if frame_hyp["service"] in in_domain_services:
domain_keys.append(SEEN_SERVICES)
else:
domain_keys.append(UNSEEN_SERVICES)
for domain_key in domain_keys:
for metric_key, metric_value in frame_metric.items():
if metric_value != NAN_VAL:
if joint_acc_across_turn and metric_key in joint_metrics:
metric_collections_per_turn[domain_key][metric_key] *= metric_value
else:
metric_collections[domain_key][metric_key].append(metric_value)
if joint_acc_across_turn:
# Conduct multiwoz style evaluation that computes joint goal accuracy
# across all the slot values of all the domains for each turn.
for domain_key in metric_collections_per_turn:
for metric_key, metric_value in metric_collections_per_turn[domain_key].items():
metric_collections[domain_key][metric_key].append(metric_value)
all_metric_aggregate = {}
for domain_key, domain_metric_vals in metric_collections.items():
domain_metric_aggregate = {}
for metric_key, value_list in domain_metric_vals.items():
if value_list:
# Metrics are macro-averaged across all frames.
domain_metric_aggregate[metric_key] = round(float(np.mean(value_list)) * 100.0, 2)
else:
domain_metric_aggregate[metric_key] = NAN_VAL
all_metric_aggregate[domain_key] = domain_metric_aggregate
return all_metric_aggregate, per_frame_metric
def evaluate(
prediction_dir: str,
data_dir: str,
eval_dataset: str,
in_domain_services: set,
joint_acc_across_turn: bool,
use_fuzzy_match: bool,
) -> dict:
"""Calculate the DSTC8/SGD metrics for given data.
Args:
prediction_dir: prediction location
data_dir: ground truth data location.
eval_dataset: evaluation data split
in_domain_services: The set of services which are present in the training set.
joint_acc_across_turn: Whether to compute joint goal accuracy across turn instead of across service. Should be set to True when conducting multiwoz style evaluation.
use_fuzzy_match: Whether to use fuzzy string matching when comparing non-categorical slot values. Should be set to False when conducting multiwoz style evaluation.
Returns:
A dict mapping a metric collection name to a dict containing the values
for various metrics for all dialogues and all services
"""
with open(os.path.join(data_dir, eval_dataset, "schema.json")) as f:
eval_services = {}
list_services = json.load(f)
for service in list_services:
eval_services[service["service_name"]] = service
f.close()
dataset_ref = get_dataset_as_dict(os.path.join(data_dir, eval_dataset, "dialogues_*.json"))
dataset_hyp = get_dataset_as_dict(os.path.join(prediction_dir, "*.json"))
# has ALLSERVICE, SEEN_SERVICES, UNSEEN_SERVICES, SERVICE, DOMAIN
all_metric_aggregate, _ = get_metrics(
dataset_ref, dataset_hyp, eval_services, in_domain_services, joint_acc_across_turn, use_fuzzy_match
)
if SEEN_SERVICES in all_metric_aggregate:
logging.info(f'Dialog metrics for {SEEN_SERVICES} : {sorted(all_metric_aggregate[SEEN_SERVICES].items())}')
if UNSEEN_SERVICES in all_metric_aggregate:
logging.info(f'Dialog metrics for {UNSEEN_SERVICES}: {sorted(all_metric_aggregate[UNSEEN_SERVICES].items())}')
if ALL_SERVICES in all_metric_aggregate:
logging.info(f'Dialog metrics for {ALL_SERVICES} : {sorted(all_metric_aggregate[ALL_SERVICES].items())}')
# Write the per-frame metrics values with the corrresponding dialogue frames.
with open(os.path.join(prediction_dir, PER_FRAME_OUTPUT_FILENAME), "w") as f:
json.dump(dataset_hyp, f, indent=2, separators=(",", ": "))
f.close()
return all_metric_aggregate[ALL_SERVICES]
@@ -0,0 +1,399 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/baseline/data_utils.py
"""
from typing import List
from nemo.utils import logging
__all__ = ['InputExample', 'STR_DONTCARE', 'STATUS_OFF', 'STATUS_ACTIVE', 'STATUS_DONTCARE']
STR_DONTCARE = "dontcare"
# These are used to represent the status of slots (off, active, dontcare) and
# intents (off, active) in dialogue state tracking.
STATUS_OFF = 0
STATUS_ACTIVE = 1
STATUS_DONTCARE = 2
class InputExample(object):
"""An example for training/inference."""
def __init__(
self,
schema_config: dict,
tokenizer: object,
service_schema: object = None,
example_id: str = "NONE",
example_id_num: List[int] = [],
):
"""
Constructs an InputExample.
Args:
schema_config: configuration
tokenizer: tokenizer object
service_schema: A ServiceSchema object wrapping the schema for the service
corresponding to this example.
example_id: Unique identifier for the example, like: 'train-1_00000-00-Restaurants_1'
example_id_num: dialogue_id and turn_id combined and service id combined into a list of ints,
like: [1, 0, 0, 18]
"""
self.schema_config = schema_config
self.service_schema = service_schema
self.service_id = None
if service_schema:
self.service_id = service_schema.service_id
self.example_id = example_id
self.example_id_num = example_id_num
self._max_seq_length = schema_config["MAX_SEQ_LENGTH"]
self._tokenizer = tokenizer
if self._tokenizer is None:
raise ValueError("Must specify tokenizer")
self.user_utterance = ''
self.system_utterance = ''
# The id of each subword in the vocabulary for BERT.
self.utterance_ids = [0] * self._max_seq_length
# Denotes the identity of the sequence. Takes values 0 (schema description) and 1 (system and user utterance).
self.utterance_segment = [0] * self._max_seq_length
# Mask which takes the value 0 for padded tokens and 1 otherwise.
self.utterance_mask = [0] * self._max_seq_length
# Start and inclusive end character indices in the original utterance
# corresponding to the tokens. This is used to obtain the character indices
# from the predicted subword indices during inference.
# NOTE: A positive value indicates the character indices in the schema description
# whereas a negative value indicates the character indices in the
# utterance. The indices are offset by 1 to prevent ambiguity in the
# 0 index, which could be in either the schema description or utterance by the
# above convention. Now the 0 index corresponds to padded tokens.
self.start_char_idx = [0] * self._max_seq_length
self.end_char_idx = [0] * self._max_seq_length
# Id of categorical slot present in the example or 0 if not present.
self.categorical_slot_id = 0
# Id of non categorical slot present in the example or 0 if not present.
self.noncategorical_slot_id = 0
# The status of categorical slot in the example.
self.categorical_slot_status = STATUS_OFF
# The status of non categorical slot in the example.
self.noncategorical_slot_status = STATUS_OFF
# Masks out tasks not represented by example
self.task_mask = [0] * schema_config["NUM_TASKS"]
# The index of the starting subword corresponding to the slot span
# for a non-categorical slot value.
self.noncategorical_slot_value_start = 0
# The index of the ending (inclusive) subword corresponding to the slot span
# for a non-categorical slot value.
self.noncategorical_slot_value_end = 0
# Id of categorical slot value present in the example or 0 if not present.
self.categorical_slot_value_id = 0
# The status of categorical slot value in the example.
self.categorical_slot_value_status = STATUS_OFF
# Id of requested slot present in the example or 0 if not present.
self.requested_slot_id = 0
# Takes value 1 if the corresponding slot is requested, 0 otherwise.
self.requested_slot_status = STATUS_OFF
# ID of intent present in the example.
self.intent_id = 0
# Takes value 1 if the intent is active, 0 otherwise.
self.intent_status = STATUS_OFF
@property
def readable_summary(self):
"""Get a readable dict that summarizes the attributes of an InputExample."""
seq_length = sum(self.utterance_mask)
utt_toks = self._tokenizer.ids_to_tokens(self.utterance_ids[:seq_length])
utt_tok_mask_pairs = list(zip(utt_toks, self.utterance_segment[:seq_length]))
active_intent = (
self.service_schema.get_intent_from_id(self.intent_id) if self.intent_status == STATUS_ACTIVE else ""
)
slot_values_in_state = {}
if self.categorical_slot_status == STATUS_ACTIVE:
slot_values_in_state[
self.service_schema.get_categorical_slot_from_id(self.categorical_slot_id)
] = self.service_schema.get_categorical_slot_value_from_id(
self.categorical_slot_id, self.categorical_slot_value_id
)
elif self.categorical_slot_status == STATUS_DONTCARE:
slot_values_in_state[
self.service_schema.get_categorical_slot_from_id(self.categorical_slot_id)
] = STR_DONTCARE
if self.noncategorical_slot_status == STATUS_ACTIVE:
slot = self.service_schema.get_non_categorical_slot_from_id(self.noncategorical_slot_id)
start_id = self.noncategorical_slot_value_start[idx]
end_id = self.noncategorical_slot_value_end[idx]
# Token list is consisted of the subwords that may start with "##". We
# remove "##" to reconstruct the original value. Note that it's not a
# strict restoration of the original string. It's primarily used for
# debugging.
# ex. ["san", "j", "##ose"] --> "san jose"
readable_value = " ".join(utt_toks[start_id : end_id + 1]).replace(" ##", "")
slot_values_in_state[slot] = readable_value
elif self.noncategorical_slot_status == STATUS_DONTCARE:
slot = self.service_schema.get_non_categorical_slot_from_id(self.noncategorical_slot_id)
slot_values_in_state[slot] = STR_DONTCARE
summary_dict = {
"utt_tok_mask_pairs": utt_tok_mask_pairs,
"utt_len": seq_length,
"categorical_slot_id": self.categorical_slot_id,
"noncategorical_slot_id": self.noncategorical_slot_id,
"intent_id": self.intent_id,
"service_name": self.service_schema.service_name,
"active_intent": active_intent,
"slot_values_in_state": slot_values_in_state,
}
return summary_dict
def add_utterance_features(
self, system_tokens, system_inv_alignments, user_tokens, user_inv_alignments, system_utterance, user_utterance
):
"""Add utterance related features input to InputExample.
Note: this method modifies the system tokens and user_tokens in place to
make their total length <= the maximum input length for BERT model.
Args:
system_tokens: a list of strings which represents schema description.
system_inv_alignments: a list of tuples which denotes the start and end
charater of the tpken that a bert token originates from in the original
schema description.
user_tokens: a list of strings which represents utterance.
user_inv_alignments: a list of tuples which denotes the start and end
charater of the token that a bert token originates from in the original
system and user utterance.
"""
# Input sequence length for utterance BERT encoder
max_utt_len = self._max_seq_length
# Modify lengths of schema description & utterance so that length of total utt
# (including cls_token, setp_token, sep_token) is no more than max_utt_len
is_too_long = truncate_seq_pair(system_tokens, user_tokens, max_utt_len - 3)
if is_too_long:
logging.debug(
f'Utterance sequence truncated in example id - {self.example_id} from {len(system_tokens) + len(user_tokens)}.'
)
# Construct the tokens, segment mask and valid token mask which will be
# input to BERT, using the tokens for schema description (sequence A) and
# system and user utterance (sequence B).
utt_subword = []
utt_seg = []
utt_mask = []
start_char_idx = []
end_char_idx = []
utt_subword.append(self._tokenizer.cls_token)
utt_seg.append(0)
utt_mask.append(1)
start_char_idx.append(0)
end_char_idx.append(0)
for subword_idx, subword in enumerate(system_tokens):
utt_subword.append(subword)
utt_seg.append(0)
utt_mask.append(1)
st, en = system_inv_alignments[subword_idx]
start_char_idx.append(-(st + 1))
end_char_idx.append(-(en + 1))
utt_subword.append(self._tokenizer.sep_token)
utt_seg.append(0)
utt_mask.append(1)
start_char_idx.append(0)
end_char_idx.append(0)
for subword_idx, subword in enumerate(user_tokens):
utt_subword.append(subword)
utt_seg.append(1)
utt_mask.append(1)
st, en = user_inv_alignments[subword_idx]
start_char_idx.append(st + 1)
end_char_idx.append(en + 1)
utt_subword.append(self._tokenizer.sep_token)
utt_seg.append(1)
utt_mask.append(1)
start_char_idx.append(0)
end_char_idx.append(0)
utterance_ids = self._tokenizer.tokens_to_ids(utt_subword)
# Zero-pad up to the BERT input sequence length.
while len(utterance_ids) < max_utt_len:
utterance_ids.append(0)
utt_seg.append(0)
utt_mask.append(0)
start_char_idx.append(0)
end_char_idx.append(0)
self.utterance_ids = utterance_ids
self.utterance_segment = utt_seg
self.utterance_mask = utt_mask
self.start_char_idx = start_char_idx
self.end_char_idx = end_char_idx
self.user_utterance = user_utterance
self.system_utterance = system_utterance
def make_copy(self):
"""Make a copy of the current example with utterance features."""
new_example = InputExample(
schema_config=self.schema_config,
service_schema=self.service_schema,
example_id=self.example_id,
example_id_num=self.example_id_num.copy(),
tokenizer=self._tokenizer,
)
return new_example
def make_copy_of_categorical_features(self):
"""Make a copy of the current example with utterance and categorical features."""
new_example = self.make_copy()
new_example.categorical_slot_status = self.categorical_slot_status
return new_example
def make_copy_of_non_categorical_features(self):
"""Make a copy of the current example with utterance features and non categorical features."""
new_example = self.make_copy()
new_example.noncategorical_slot_id = self.noncategorical_slot_id
new_example.noncategorical_slot_status = self.noncategorical_slot_status
new_example.utterance_ids = list(self.utterance_ids)
new_example.utterance_segment = list(self.utterance_segment)
new_example.utterance_mask = list(self.utterance_mask)
new_example.start_char_idx = list(self.start_char_idx)
new_example.end_char_idx = list(self.end_char_idx)
new_example.user_utterance = self.user_utterance
new_example.system_utterance = self.system_utterance
new_example.noncategorical_slot_status = self.noncategorical_slot_status
new_example.noncategorical_slot_value_start = self.noncategorical_slot_value_start
new_example.noncategorical_slot_value_end = self.noncategorical_slot_value_end
return new_example
def add_categorical_slots(self, state_update: dict):
"""Add features for categorical slots.
Args:
state_update: slot value pairs of the state update
"""
categorical_slots = self.service_schema.categorical_slots
if not categorical_slots:
return
slot = categorical_slots[self.categorical_slot_id]
values = state_update.get(slot, [])
if not values:
self.categorical_slot_status = STATUS_OFF
elif values[0] == STR_DONTCARE:
self.categorical_slot_status = STATUS_DONTCARE
else:
self.categorical_slot_status = STATUS_ACTIVE
self.categorical_slot_value_status = (
self.categorical_slot_value_id == self.service_schema.get_categorical_slot_value_id(slot, values[0])
)
def add_noncategorical_slots(self, state_update: dict, system_span_boundaries: dict, user_span_boundaries: dict):
"""Add features for non-categorical slots.
Args:
state_update: slot value pairs of state update
system_span_boundaries: span boundaries of schema description
user_span_boundaries: span boundaries of utterance
"""
noncategorical_slots = self.service_schema.non_categorical_slots
slot = noncategorical_slots[self.noncategorical_slot_id]
values = state_update.get(slot, [])
if not values:
self.noncategorical_slot_status = STATUS_OFF
elif values[0] == STR_DONTCARE:
self.noncategorical_slot_status = STATUS_DONTCARE
else:
self.noncategorical_slot_status = STATUS_ACTIVE
# Add indices of the start and end tokens for the first encountered
# value. Spans in user utterance are prioritized over the system
# utterance. If a span is not found, the slot value is ignored.
if slot in user_span_boundaries:
start, end = user_span_boundaries[slot]
elif slot in system_span_boundaries:
start, end = system_span_boundaries[slot]
else:
# A span may not be found because the value was cropped out or because
# the value was mentioned earlier in the dialogue. Since this model
# only makes use of the last two utterances to predict state updates,
# it will fail in such cases.
logging.debug(
f'"Slot values {str(values)} not found in user or system utterance in example with id - {self.example_id}.'
)
start = 0
end = 0
self.noncategorical_slot_value_start = start
self.noncategorical_slot_value_end = end
def add_requested_slots(self, frame: dict):
"""Add requested slots to InputExample
Args:
frame: frame object from which requested slots are extracted
"""
all_slots = self.service_schema.slots
slot = all_slots[self.requested_slot_id]
if slot in frame["state"]["requested_slots"]:
self.requested_slot_status = STATUS_ACTIVE
def add_intents(self, frame):
"""Add intents to InputExample
Args:
frame: frame object from which intents are extracted
"""
all_intents = self.service_schema.intents
intent = all_intents[self.intent_id]
if intent == frame["state"]["active_intent"]:
self.intent_status = STATUS_ACTIVE
# Modified from run_classifier._truncate_seq_pair in the public bert model repo.
# https://github.com/google-research/bert/blob/master/run_classifier.py.
def truncate_seq_pair(tokens_a: List[int], tokens_b: List[int], max_length: int) -> bool:
"""Truncate a seq pair in place so that their total length <= max_length.
Args:
tokens_a: first token sequence
tokens_b: second token sequence
max_length: truncated sequence length
Returns:
is_too_long: whether combined sequences exceed maximum sequence length
"""
is_too_long = False
# This is a simple heuristic which will always truncate the longer sequence
# one token at a time. This makes more sense than truncating an equal percent
# of tokens from each, since if one sequence is very short then each token
# that's truncated likely contains more information than a longer sequence.
while True:
total_length = len(tokens_a) + len(tokens_b)
if total_length <= max_length:
break
is_too_long = True
if len(tokens_a) > len(tokens_b):
tokens_a.pop()
else:
tokens_b.pop()
return is_too_long
@@ -0,0 +1,341 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Evaluation metrics for Schema-guided dialogue.
This library provides functions for calculating the evaluation metrics for a
single dialogue. The following metrics are defined:
(1) Active intent accuracy: The fraction of user turns for which the active
intent has been correctly predicted.
(2) Slot tagging F1: The macro-averaged F1 score for tagging slot values for
non-categorical slots. This metric is optional to report in the final paper
if participants decide not to use slot tagging.
(3) Requested slots F1: The macro-averaged F1 score for requested slots over the
turns. For a turn, if there are no requested slots in both the ground truth
and the prediction, that turn is skipped. The reported number is the average
F1 score for all un-skipped user turns. This metric is optional to report in
the final paper.
(4) Average goal accuracy: For each turn, participants must predict a single
value for each slot present in the dialogue state. The slots which have a
non-empty assignment in the ground truth dialogue state are only considered.
This is the average accuracy of predicting the value of a slot correctly. A
fuzzy matching based score is used for non-categorical slots.
(5) Joint goal accuracy: This is the average accuracy of predicting all slot
assignments for a turn correctly. A fuzzy matching based score is used for
non-categorical slots. This is the primary evaluation metric used for ranking
submissions. More details to follow with the evaluation script.
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/metrics.py
"""
import collections
import numpy as np
from rapidfuzz import fuzz
F1Scores = collections.namedtuple("F1Scores", ["f1", "precision", "recall"])
# Evaluation and other relevant metrics for DSTC8/SGD Schema-guided DST.
# (1) Active intent accuracy.
ACTIVE_INTENT_ACCURACY = "active_intent_accuracy"
# (2) Slot tagging F1.
SLOT_TAGGING_F1 = "slot_tagging_f1"
SLOT_TAGGING_PRECISION = "slot_tagging_precision"
SLOT_TAGGING_RECALL = "slot_tagging_recall"
# (3) Requested slots F1.
REQUESTED_SLOTS_F1 = "requested_slots_f1"
REQUESTED_SLOTS_PRECISION = "requested_slots_precision"
REQUESTED_SLOTS_RECALL = "requested_slots_recall"
# (4) Average goal accuracy.
AVERAGE_GOAL_ACCURACY = "average_goal_accuracy"
AVERAGE_CAT_ACCURACY = "average_cat_accuracy"
AVERAGE_NONCAT_ACCURACY = "average_noncat_accuracy"
# (5) Joint goal accuracy.
JOINT_GOAL_ACCURACY = "joint_goal_accuracy"
JOINT_CAT_ACCURACY = "joint_cat_accuracy"
JOINT_NONCAT_ACCURACY = "joint_noncat_accuracy"
AVERAGE_CAT_STATUS_ACCURACY = "average_cat_status_accuracy"
AVERAGE_CAT_VALUE_ACCURACY = "average_cat_value_accuracy"
AVERAGE_NONCAT_STATUS_ACCURACY = "average_noncat_status_accuracy"
AVERAGE_NONCAT_VALUE_ACCURACY = "average_noncat_value_accuracy"
JOINT_CAT_STATUS_ACCURACY = "joint_cat_status_accuracy"
JOINT_CAT_VALUE_ACCURACY = "joint_cat_value_accuracy"
JOINT_NONCAT_STATUS_ACCURACY = "joint_noncat_status_accuracy"
JOINT_NONCAT_VALUE_ACCURACY = "joint_noncat_value_accuracy"
NAN_VAL = "NA"
def compute_f1(list_ref, list_hyp):
"""Compute F1 score from reference (grouth truth) list and hypothesis list.
Args:
list_ref: List of true elements.
list_hyp: List of postive (retrieved) elements.
Returns:
A F1Scores object containing F1, precision, and recall scores.
"""
ref = collections.Counter(list_ref)
hyp = collections.Counter(list_hyp)
true = sum(ref.values())
positive = sum(hyp.values())
true_positive = sum((ref & hyp).values())
precision = float(true_positive) / positive if positive else 1.0
recall = float(true_positive) / true if true else 1.0
if precision + recall > 0.0:
f1 = 2.0 * precision * recall / (precision + recall)
else: # The F1-score is defined to be 0 if both precision and recall are 0.
f1 = 0.0
return F1Scores(f1=f1, precision=precision, recall=recall)
def fuzzy_string_match(str_ref, str_hyp):
"""Returns fuzzy string similarity score in range [0.0, 1.0].
Args:
str_ref: reference string
str_hyp: hypothesis string
Returns:
fuzzy string similarity
"""
# The higher the score, the higher the similarity between the two strings.
return fuzz.token_sort_ratio(str_ref, str_hyp) / 100.0
def noncat_slot_value_match(str_ref_list, str_hyp, use_fuzzy_match):
"""Calculate non-categorical slots correctness.
Args:
str_ref_list: a list of reference strings.
str_hyp: the hypothesis string.
use_fuzzy_match: whether to use fuzzy string matching.
Returns:
score: The highest fuzzy string match score of the references and hypotheis.
"""
score = 0.0
for str_ref in str_ref_list:
if use_fuzzy_match:
match_score = fuzzy_string_match(str_ref, str_hyp)
else:
match_score = float(str_ref == str_hyp)
score = max(score, match_score)
return score
def compare_slot_values(slot_values_ref, slot_values_hyp, service, use_fuzzy_match):
"""Compare and get correctness of goal state's slot_values.
Args:
slot_values_ref: goal state slot_values from reference (ground truth).
slot_values_hyp: goal state slot_values from hypothesis (prediction).
service: a service data structure in the schema. We use it to obtain the
list of slots in the service and infer whether a slot is categorical.
use_fuzzy_match: whether to use fuzzy string matching for non-categorical
slot values
Returns:
list_cor: list of corectness scores, each corresponding to one slot in the
service. The score is a float either 0.0 or 1.0 for categorical slot,
and in range [0.0, 1.0] for non-categorical slot.
slot_active: list indicating whether the element in list_cor corresponds to
an active ground-truth slot.
slot_cat: list indicating whether the element in list_cor corresponds to a
categorical slot.
list_cor_status: list of correct slot statuses
list_cor_value: list of correctness score only for active slots. Monactive slots are assigned -1.
"""
list_cor = []
list_cor_status = []
list_cor_value = []
slot_active = []
slot_cat = []
for slot in service["slots"]:
slot_name = slot["name"]
slot_cat.append(slot["is_categorical"])
if slot_name in slot_values_ref: # REF=active
slot_active.append(True)
if slot_name in slot_values_hyp: # HYP=active, apply matching
value_ref_list = slot_values_ref[slot_name]
value_hyp = slot_values_hyp[slot_name][0]
if slot["is_categorical"]:
cor = float(value_ref_list[0] == value_hyp)
else:
cor = noncat_slot_value_match(value_ref_list, value_hyp, use_fuzzy_match)
list_cor.append(cor)
list_cor_status.append(1.0)
list_cor_value.append(cor)
else: # HYP=off
list_cor.append(0.0)
list_cor_status.append(0.0)
list_cor_value.append(-1.0)
else: # REF=off
slot_active.append(False)
if slot_name in slot_values_hyp: # HYP=active
list_cor.append(0.0)
list_cor_status.append(0.0)
else: # HYP=off
list_cor.append(1.0)
list_cor_status.append(1.0)
list_cor_value.append(-1.0)
assert len(list_cor) == len(service["slots"])
assert len(slot_active) == len(service["slots"])
assert len(slot_cat) == len(service["slots"])
return list_cor, slot_active, slot_cat, list_cor_status, list_cor_value
def get_active_intent_accuracy(frame_ref, frame_hyp):
"""Get active intent accuracy of a frame.
Args:
frame_ref: single semantic frame from reference (ground truth) file.
frame_hyp: single semantic frame from hypothesis (prediction) file.
Returns:
1.0 if the intent prediction is correct, otherwise 0.0.
"""
return float(frame_ref["state"]["active_intent"] == frame_hyp["state"]["active_intent"])
def get_slot_tagging_f1(frame_ref, frame_hyp, utt, service):
"""Get slot tagging (non-categorical slots only) F1 scores of a frame.
Args:
frame_ref: single semantic frame from reference (ground truth) file.
frame_hyp: single semantic frame from hypothesis (prediction) file.
utt: user utterance. Slot tagging annotations are the character positions in
the utterance.
service: a service data structure in the schema. We use it to infer whether
a slot is non-categorical.
Returns:
A F1Scores object containing F1, precision, and recall scores.
"""
list_noncat_slots = [s["name"] for s in service["slots"] if not s["is_categorical"]]
if "slots" not in frame_hyp:
return None
else:
list_ref = [
(s["slot"], utt[s["start"] : s["exclusive_end"]])
for s in frame_ref["slots"]
if s["slot"] in list_noncat_slots
]
list_hyp = [
(s["slot"], utt[s["start"] : s["exclusive_end"]])
for s in frame_hyp["slots"]
if s["slot"] in list_noncat_slots
]
return compute_f1(list_ref, list_hyp)
def get_requested_slots_f1(frame_ref, frame_hyp):
"""Get requested slots F1 scores of a frame.
Args:
frame_ref: single semantic frame from reference (ground truth) file.
frame_hyp: single semantic frame from hypothesis (prediction) file.
Returns:
A F1Scores object containing F1, precision, and recall scores.
"""
return compute_f1(frame_ref["state"]["requested_slots"], frame_hyp["state"]["requested_slots"])
def get_average_and_joint_goal_accuracy(frame_ref, frame_hyp, service, use_fuzzy_match):
"""Get average and joint goal accuracies of a frame.
Args:
frame_ref: single semantic frame from reference (ground truth) file.
frame_hyp: single semantic frame from hypothesis (prediction) file.
service: a service data structure in the schema. We use it to obtain the
list of slots in the service and infer whether a slot is categorical.
use_fuzzy_match: whether to use fuzzy string matching for comparing
non-categorical slot values.
Returns:
goal_acc: a dict whose values are average / joint
all-goal / categorical-goal / non-categorical-goal accuracies.
"""
goal_acc = {}
list_acc, slot_active, slot_cat, list_status_acc, list_value_acc = compare_slot_values(
frame_ref["state"]["slot_values"], frame_hyp["state"]["slot_values"], service, use_fuzzy_match
)
# (4) Average goal accuracy.
active_acc = [acc for acc, active in zip(list_acc, slot_active) if active]
goal_acc[AVERAGE_GOAL_ACCURACY] = np.mean(active_acc) if active_acc else NAN_VAL
# (4-a) categorical.
active_cat_acc = [acc for acc, active, cat in zip(list_acc, slot_active, slot_cat) if active and cat]
goal_acc[AVERAGE_CAT_ACCURACY] = np.mean(active_cat_acc) if active_cat_acc else NAN_VAL
# (4-b) non-categorical.
active_noncat_acc = [acc for acc, active, cat in zip(list_acc, slot_active, slot_cat) if active and not cat]
goal_acc[AVERAGE_NONCAT_ACCURACY] = np.mean(active_noncat_acc) if active_noncat_acc else NAN_VAL
# (5) Joint goal accuracy.
goal_acc[JOINT_GOAL_ACCURACY] = np.prod(list_acc) if list_acc else NAN_VAL
# (5-a) categorical.
cat_acc = [acc for acc, cat in zip(list_acc, slot_cat) if cat]
goal_acc[JOINT_CAT_ACCURACY] = np.prod(cat_acc) if cat_acc else NAN_VAL
# (5-b) non-categorical.
noncat_acc = [acc for acc, cat in zip(list_acc, slot_cat) if not cat]
goal_acc[JOINT_NONCAT_ACCURACY] = np.prod(noncat_acc) if noncat_acc else NAN_VAL
# !!!!!!!!!!DEBUG!!!!!!!!!!!!!
# cat status acc for both active and non active
active_cat_status_acc = [acc for acc, active, cat in zip(list_status_acc, slot_active, slot_cat) if cat and active]
goal_acc[AVERAGE_CAT_STATUS_ACCURACY] = np.mean(active_cat_status_acc) if active_cat_status_acc else NAN_VAL
# joint cat status acc for both active and non active
cat_status_acc = [acc for acc, cat in zip(list_status_acc, slot_cat) if cat]
goal_acc[JOINT_CAT_STATUS_ACCURACY] = np.prod(cat_status_acc) if cat_status_acc else NAN_VAL
# non cat status acc for both active and non active
active_noncat_status_acc = [
acc for acc, active, cat in zip(list_status_acc, slot_active, slot_cat) if not cat and active
]
goal_acc[AVERAGE_NONCAT_STATUS_ACCURACY] = (
np.mean(active_noncat_status_acc) if active_noncat_status_acc else NAN_VAL
)
# joint non cat status acc for both active and non active
noncat_status_acc = [acc for acc, cat in zip(list_status_acc, slot_cat) if not cat]
goal_acc[JOINT_NONCAT_STATUS_ACCURACY] = np.prod(noncat_status_acc) if noncat_status_acc else NAN_VAL
# cat value acc for both active and non active
active_cat_val_acc = [
acc for acc, active, cat in zip(list_value_acc, slot_active, slot_cat) if cat and acc > -0.5 and active
]
goal_acc[AVERAGE_CAT_VALUE_ACCURACY] = np.mean(active_cat_val_acc) if active_cat_val_acc else NAN_VAL
# joint cat value acc for both active and non active
cat_val_acc = [acc for acc, cat in zip(list_value_acc, slot_cat) if cat and acc > -0.5]
goal_acc[JOINT_CAT_VALUE_ACCURACY] = np.prod(cat_val_acc) if cat_val_acc else NAN_VAL
# cat non value acc for both active and non active
active_noncat_val_acc = [
acc for acc, active, cat in zip(list_value_acc, slot_active, slot_cat) if not cat and acc > -0.5 and active
]
goal_acc[AVERAGE_NONCAT_VALUE_ACCURACY] = np.mean(active_noncat_val_acc) if active_noncat_val_acc else NAN_VAL
# joint non cat value acc for both active and non active
noncat_val_acc = [acc for acc, cat in zip(list_value_acc, slot_cat) if not cat and acc > -0.5]
goal_acc[JOINT_NONCAT_VALUE_ACCURACY] = np.prod(noncat_val_acc) if noncat_val_acc else NAN_VAL
return goal_acc
@@ -0,0 +1,251 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Prediction and evaluation-related utility functions.
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/baseline/pred_utils.py
"""
import json
import os
from collections import OrderedDict, defaultdict
from typing import Dict, List, Optional
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.input_example import (
STATUS_ACTIVE,
STATUS_DONTCARE,
STR_DONTCARE,
)
from nemo.utils import logging
REQ_SLOT_THRESHOLD = 0.5
__all__ = ['write_predictions_to_file']
def set_cat_slot(predictions_status: dict, predictions_value: dict, cat_slot_values: Dict[str, List[str]]) -> dict:
"""
Extract predicted categorical slot information
Args:
predictions_status: predicted statuses
predictions_value: predicted slot values
cat_slot_values: possible categorical slots and their potential values for this service
Returns:
out_dict: predicted slot value pairs
"""
out_dict = {}
for slot_idx, slot in enumerate(cat_slot_values):
slot_status = predictions_status[slot_idx][0]["cat_slot_status"]
if slot_status == STATUS_DONTCARE:
out_dict[slot] = STR_DONTCARE
elif slot_status == STATUS_ACTIVE:
tmp = predictions_value[slot_idx]
value_idx = max(tmp, key=lambda k: tmp[k]['cat_slot_value_status'][0].item())
out_dict[slot] = cat_slot_values[slot][value_idx]
return out_dict
def set_noncat_slot(
predictions_status: dict,
predictions_value: dict,
non_cat_slots: List[str],
user_utterance: str,
sys_slots_agg: Optional[dict] = None,
) -> dict:
"""
Extract predicted non categorical slot information
Args:
predictions_status: predicted statuses
predictions_value: predicted slot values
non_cat_slots: list of possible non categorical slots for this service
user_utterance: system and user utterance
sys_slots_agg: system retrieval lookup table. Contains for each slot the most recent value seen in the history
Returns:
out_dict: predicted slot value pairs
"""
out_dict = {}
for slot_idx, slot in enumerate(non_cat_slots):
slot_status = predictions_status[slot_idx][0]["noncat_slot_status"]
if slot_status == STATUS_DONTCARE:
out_dict[slot] = STR_DONTCARE
elif slot_status == STATUS_ACTIVE:
tok_start_idx = predictions_value[slot_idx][0]["noncat_slot_start"]
tok_end_idx = predictions_value[slot_idx][0]["noncat_slot_end"]
ch_start_idx = predictions_value[slot_idx][0]["noncat_alignment_start"][tok_start_idx]
ch_end_idx = predictions_value[slot_idx][0]["noncat_alignment_end"][tok_end_idx]
if ch_start_idx > 0 and ch_end_idx > 0:
# Add span from the utterance.
out_dict[slot] = user_utterance[ch_start_idx - 1 : ch_end_idx]
elif sys_slots_agg and slot in sys_slots_agg:
# system retrieval
out_dict[slot] = sys_slots_agg[slot]
return out_dict
def get_predicted_dialog(dialog: dict, all_predictions: dict, schemas: object, state_tracker: str) -> dict:
"""Overwrite the labels in the turn with the predictions from the model. For test set, these labels are missing from the data and hence they are added.
Args:
dialog: ground truth dialog
all_predictions: predictions
schemas: schema object of all services of all datasets
state_tracker: state tracker option, e.g. nemotracker
Returns:
dialog: dialog overwritten with prediction information
"""
dialog_id = dialog["dialogue_id"]
if state_tracker == "baseline":
sys_slots_agg = {}
else:
sys_slots_agg = defaultdict(OrderedDict)
all_slot_values = defaultdict(dict)
for turn_idx, turn in enumerate(dialog["turns"]):
if turn["speaker"] == "SYSTEM" and state_tracker == 'nemotracker':
for frame in turn["frames"]:
if frame["service"] not in sys_slots_agg:
sys_slots_agg[frame["service"]] = OrderedDict()
for action in frame["actions"]:
if action["slot"] and len(action["values"]) > 0:
sys_slots_agg[frame["service"]][action["slot"]] = action["values"][0]
if turn["speaker"] == "USER":
user_utterance = turn["utterance"]
system_utterance = dialog["turns"][turn_idx - 1]["utterance"] if turn_idx else ""
system_user_utterance = system_utterance + ' ' + user_utterance
turn_id = "{:02d}".format(turn_idx)
for frame in turn["frames"]:
predictions = all_predictions[(dialog_id, turn_id, frame["service"])]
slot_values = all_slot_values[frame["service"]]
service_schema = schemas.get_service_schema(frame["service"])
# Remove the slot spans and state if present.
frame.pop("slots", None)
frame.pop("state", None)
# The baseline model doesn't predict slot spans. Only state predictions
# are added.
state = {}
# Add prediction for active intent. No Offset is subtracted since schema has now NONE intent at index 0
state["active_intent"] = get_predicted_intent(
predictions=predictions[0], intents=service_schema.intents
)
# Add prediction for requested slots.
state["requested_slots"] = get_requested_slot(predictions=predictions[1], slots=service_schema.slots)
# Add prediction for user goal (slot values).
# Categorical slots.
cat_out_dict = set_cat_slot(
predictions_status=predictions[2],
predictions_value=predictions[3],
cat_slot_values=service_schema.categorical_slot_values,
)
for k, v in cat_out_dict.items():
slot_values[k] = v
# Non-categorical slots.
noncat_out_dict = set_noncat_slot(
predictions_status=predictions[4],
predictions_value=predictions[5],
non_cat_slots=service_schema.non_categorical_slots,
user_utterance=system_user_utterance,
sys_slots_agg=sys_slots_agg.get(frame["service"], None),
)
for k, v in noncat_out_dict.items():
slot_values[k] = v
# Create a new dict to avoid overwriting the state in previous turns
# because of use of same objects.
state["slot_values"] = {s: [v] for s, v in slot_values.items()}
frame["state"] = state
return dialog
def get_predicted_intent(predictions: dict, intents: List[str]) -> str:
"""
Returns intent name with maximum score
Args:
predictions: predictions
intents: list of possible intents for this service
Returns:
intent: predicted intent
"""
assert len(predictions) == len(intents)
active_intent_id = max(predictions, key=lambda k: predictions[k][0]['intent_status'])
intent = intents[active_intent_id]
return intent
def get_requested_slot(predictions: dict, slots: List[str]) -> List[str]:
"""
Returns list of slots which are predicted to be requested
Args:
predictions: predictions
slots: list of possible slots
Returns:
requested_slots: list of requested slots
"""
active_indices = [k for k in predictions if predictions[k][0]["req_slot_status"] > REQ_SLOT_THRESHOLD]
requested_slots = list(map(lambda k: slots[k], active_indices))
return requested_slots
def write_predictions_to_file(
predictions: List[dict],
input_json_files: List[str],
output_dir: str,
schemas: object,
state_tracker: str,
eval_debug: bool,
in_domain_services: set,
):
"""Save predicted dialogues as json files.
Args:
predictions: An iterator containing model predictions. This is the output of
the predict method in the estimator.
input_json_files: A list of json paths containing the dialogues to run
inference on.
output_dir: The directory where output json files will be created.
schemas: Schemas to all services in the dst dataset
state_tracker: state tracker option
eval_debug: output evaluation debugging information
in_domain_services: in domain services
"""
logging.info(f"Writing predictions to {output_dir} started.")
# Index all predictions.
all_predictions = defaultdict(lambda: defaultdict(lambda: defaultdict(dict)))
for idx, prediction in enumerate(predictions):
eval_dataset, dialog_id, turn_id, service_name, model_task, slot_intent_id, value_id = prediction[
'example_id'
].split('-')
all_predictions[(dialog_id, turn_id, service_name)][int(model_task)][int(slot_intent_id)][
int(value_id)
] = prediction
logging.info(f'Predictions for {idx} examples in {eval_dataset} dataset are getting processed.')
# Read each input file and write its predictions.
for input_file_path in input_json_files:
with open(input_file_path) as f:
dialogs = json.load(f)
logging.debug(f'{input_file_path} file is loaded')
pred_dialogs = []
for d in dialogs:
pred_dialog = get_predicted_dialog(d, all_predictions, schemas, state_tracker)
pred_dialogs.append(pred_dialog)
input_file_name = os.path.basename(input_file_path)
output_file_path = os.path.join(output_dir, input_file_name)
with open(output_file_path, "w") as f:
json.dump(pred_dialogs, f, indent=2, separators=(",", ": "), sort_keys=True)
@@ -0,0 +1,222 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Wrappers for schemas of different services.
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/schema.py
"""
import json
from typing import List, Optional, Union
from nemo.utils import logging
__all__ = ['Schema']
class ServiceSchema(object):
"""A wrapper for schema for a service."""
def __init__(self, schema_json: dict, service_id: Optional[int] = None):
"""
Constructor for ServiceSchema.
Args:
schema_json: schema json dict
service_id: service ID
"""
self._service_name = schema_json["service_name"]
self._description = schema_json["description"]
self._schema_json = schema_json
self._service_id = service_id
# Construct the vocabulary for intents, slots, categorical slots,
# non-categorical slots and categorical slot values.
self._intents = ["NONE"] + sorted(i["name"] for i in schema_json["intents"])
self._intent_descriptions = {i["name"]: i["description"] for i in schema_json["intents"]}
self._intent_descriptions["NONE"] = "none"
self._slots = sorted(s["name"] for s in schema_json["slots"])
self._slots_descriptions = {s["name"]: s["description"] for s in schema_json["slots"]}
self._categorical_slots = sorted(
s["name"] for s in schema_json["slots"] if s["is_categorical"] and s["name"] in self.state_slots
)
self._non_categorical_slots = sorted(
s["name"] for s in schema_json["slots"] if not s["is_categorical"] and s["name"] in self.state_slots
)
slot_schemas = {s["name"]: s for s in schema_json["slots"]}
categorical_slot_values = {}
categorical_slot_value_ids = {}
categorical_slot_ids = {}
non_categorical_slot_ids = {}
for slot_id, slot in enumerate(self._categorical_slots):
slot_schema = slot_schemas[slot]
values = sorted(slot_schema["possible_values"])
categorical_slot_values[slot] = values
value_ids = {value: idx for idx, value in enumerate(values)}
categorical_slot_value_ids[slot] = value_ids
categorical_slot_ids[slot] = slot_id
for slot_id, slot in enumerate(self._non_categorical_slots):
non_categorical_slot_ids[slot] = slot_id
self._categorical_slot_values = categorical_slot_values
self._categorical_slot_value_ids = categorical_slot_value_ids
self._categorical_slot_ids = categorical_slot_ids
self._non_categorical_slot_ids = non_categorical_slot_ids
@property
def schema_json(self) -> dict:
"""Returns schema json dictionary"""
return self._schema_json
@property
def state_slots(self) -> set:
"""Set of slots which are permitted to be in the dialogue state."""
state_slots = set()
for intent in self._schema_json["intents"]:
state_slots.update(intent["required_slots"])
state_slots.update(intent["optional_slots"])
return state_slots
@property
def service_name(self):
return self._service_name
@property
def service_id(self):
return self._service_id
@property
def description(self):
return self._description
@property
def slots(self):
return self._slots
@property
def intents(self):
return self._intents
@property
def intent_descriptions(self):
return self._intent_descriptions
@property
def slot_descriptions(self):
return self._slots_descriptions
@property
def categorical_slots(self):
return self._categorical_slots
@property
def non_categorical_slots(self):
return self._non_categorical_slots
@property
def categorical_slot_values(self):
return self._categorical_slot_values
def get_categorical_slot_values(self, slot):
return self._categorical_slot_values[slot]
def get_slot_from_id(self, slot_id):
return self._slots[slot_id]
def get_intent_from_id(self, intent_id):
return self._intents[intent_id]
def get_categorical_slot_from_id(self, slot_id):
return self._categorical_slots[slot_id]
def get_non_categorical_slot_from_id(self, slot_id):
return self._non_categorical_slots[slot_id]
def get_categorical_slot_value_from_id(self, slot_id, value_id):
slot = self._categorical_slots[slot_id]
return self._categorical_slot_values[slot][value_id]
def get_categorical_slot_value_id(self, slot, value):
return self._categorical_slot_value_ids[slot][value]
def get_categorical_slot_id(self, slot):
return self._categorical_slot_ids[slot]
def get_non_categorical_slot_id(self, slot):
return self._non_categorical_slot_ids[slot]
class Schema(object):
"""Wrapper for schemas for all services in a dataset."""
def __init__(self, schema_json_paths: Union[str, List[str]]):
"""
schema_json_paths: list of .json path to schema files of a single str with path to the json file.
"""
# Load the schema from the json file.
if isinstance(schema_json_paths, str):
with open(schema_json_paths, "r") as f:
all_schemas = json.load(f)
f.close()
else:
# load multiple schemas from the list of the json files
all_schemas = []
completed_services = []
for schema_json_path in schema_json_paths:
with open(schema_json_path, "r") as f:
schemas = json.load(f)
f.close()
logging.debug("Num of services in %s: %s", schema_json_path, len(schemas))
for service in schemas:
if service['service_name'] not in completed_services:
completed_services.append(service['service_name'])
all_schemas.append(service)
self._services = sorted(schema["service_name"] for schema in all_schemas)
self._services_vocab = {v: k for k, v in enumerate(self._services)}
self._services_id_to_vocab = {v: k for k, v in self._services_vocab.items()}
service_schemas = {}
for schema in all_schemas:
service = schema["service_name"]
service_schemas[service] = ServiceSchema(schema, service_id=self.get_service_id(service))
self._service_schemas = service_schemas
self._schemas = all_schemas
self._slots_relation_list = {}
def get_service_id(self, service: str):
return self._services_vocab[service]
def get_service_from_id(self, service_id: int):
return self._services[service_id]
def get_service_schema(self, service: str):
return self._service_schemas[service]
@property
def services(self):
return self._services
def save_to_file(self, file_path):
"""
Saves schema object to file
Args:
file_path: path to store schema object at
"""
with open(file_path, "w") as f:
json.dump(self._schemas, f, indent=2)
@@ -0,0 +1,15 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.entity_linking.entity_linking_dataset import EntityLinkingDataset
@@ -0,0 +1,135 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import array
import pickle as pkl
from typing import Optional
import torch
from nemo.collections.nlp.data.data_utils.data_preprocessing import find_newlines, load_data_indices
from nemo.core.classes import Dataset
from nemo.utils import logging
__all__ = ['EntityLinkingDataset']
class EntityLinkingDataset(Dataset):
"""
Parent class for entity linking encoder training and index
datasets
Args:
tokenizer (obj): huggingface tokenizer,
data_file (str): path to tab separated column file where data
pairs apear in the format
concept_ID\tconcept_synonym1\tconcept_synonym2\n
newline_idx_file (str): path to pickle file containing location
of data_file newline characters
max_seq_length (int): maximum length of a concept in tokens
is_index_data (bool): Whether dataset will be used for building
a nearest neighbors index
"""
def __init__(
self,
tokenizer: object,
data_file: str,
newline_idx_file: Optional[str] = None,
max_seq_length: Optional[int] = 512,
is_index_data: bool = False,
):
self.tokenizer = tokenizer
# Try and load pair indices file if already exists
newline_indices, newline_idx_file, _ = load_data_indices(newline_idx_file, data_file, "newline_indices")
# If pair indices file doesn't exists, generate and store them
if newline_indices is None:
logging.info("Getting datafile newline indices")
with open(data_file, "rb") as f:
contents = f.read()
newline_indices = find_newlines(contents)
newline_indices = array.array("I", newline_indices)
# Store data file indicies to avoid generating them again
with open(newline_idx_file, "wb") as f:
pkl.dump(newline_indices, f)
self.newline_indices = newline_indices
self.data_file = data_file
self.num_lines = len(newline_indices)
self.max_seq_length = max_seq_length
self.is_index_data = is_index_data
logging.info(f"Loaded dataset with {self.num_lines} examples")
def __len__(self):
return self.num_lines
def __getitem__(self, idx):
concept_offset = self.newline_indices[idx]
with open(self.data_file, "r", encoding='utf-8-sig') as f:
# Find data pair within datafile using byte offset
f.seek(concept_offset)
concept = f.readline()[:-1]
concept = concept.strip().split("\t")
if self.is_index_data:
concept_id, concept = concept
return (int(concept_id), concept)
else:
concept_id, concept1, concept2 = concept
return (int(concept_id), concept1, concept2)
def _collate_fn(self, batch):
"""collate batch of input_ids, segment_ids, input_mask, and label
Args:
batch: A list of tuples of format (concept_ID, concept_synonym1, concept_synonym2).
"""
if self.is_index_data:
concept_ids, concepts = zip(*batch)
concept_ids = list(concept_ids)
concepts = list(concepts)
else:
concept_ids, concepts1, concepts2 = zip(*batch)
concept_ids = list(concept_ids)
concept_ids.extend(concept_ids) # Need to double label list to match each concept
concepts = list(concepts1)
concepts.extend(concepts2)
batch = self.tokenizer(
concepts,
add_special_tokens=True,
padding=True,
truncation=True,
max_length=self.max_seq_length,
return_token_type_ids=True,
return_attention_mask=True,
return_length=True,
)
return (
torch.LongTensor(batch["input_ids"]),
torch.LongTensor(batch["token_type_ids"]),
torch.LongTensor(batch["attention_mask"]),
torch.LongTensor(concept_ids),
)
@@ -0,0 +1,15 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.glue_benchmark.glue_benchmark_dataset import GLUEDataset
@@ -0,0 +1,323 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from nemo.collections.nlp.data.data_utils.data_preprocessing import DataProcessor
from nemo.utils import logging
__all__ = [
'ColaProcessor',
'MnliProcessor',
'MnliMismatchedProcessor',
'MrpcProcessor',
'Sst2Processor',
'StsbProcessor',
'QqpProcessor',
'QnliProcessor',
'RteProcessor',
'WnliProcessor',
]
class MrpcProcessor(DataProcessor):
"""Processor for the MRPC data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
logging.info(f'LOOKING AT {os.path.join(data_dir, "train.tsv")}')
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return ["0", "1"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, i)
text_a = line[3]
text_b = line[4]
label = line[0]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
class MnliProcessor(DataProcessor):
"""Processor for the MultiNLI data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev_matched.tsv")), "dev_matched")
def get_labels(self):
"""See base class."""
return ["contradiction", "entailment", "neutral"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, line[0])
text_a = line[8]
text_b = line[9]
label = line[-1]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
class MnliMismatchedProcessor(MnliProcessor):
"""Processor for the MultiNLI Mismatched data set (GLUE version)."""
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev_mismatched.tsv")), "dev_matched")
class ColaProcessor(DataProcessor):
"""Processor for the CoLA data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return ["0", "1"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
guid = "%s-%s" % (set_type, i)
text_a = line[3]
label = line[1]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
return examples
class Sst2Processor(DataProcessor):
"""Processor for the SST-2 data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return ["0", "1"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, i)
text_a = line[0]
label = line[1]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
return examples
class StsbProcessor(DataProcessor):
"""Processor for the STS-B data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return [None]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, line[0])
text_a = line[7]
text_b = line[8]
label = line[-1]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
class QqpProcessor(DataProcessor):
"""Processor for the QQP data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return ["0", "1"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, line[0])
try:
text_a = line[3]
text_b = line[4]
label = line[5]
except IndexError:
continue
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
class QnliProcessor(DataProcessor):
"""Processor for the QNLI data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return ["entailment", "not_entailment"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, line[0])
text_a = line[1]
text_b = line[2]
label = line[-1]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
class RteProcessor(DataProcessor):
"""Processor for the RTE data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return ["entailment", "not_entailment"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, line[0])
text_a = line[1]
text_b = line[2]
label = line[-1]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
class WnliProcessor(DataProcessor):
"""Processor for the WNLI data set (GLUE version)."""
def get_train_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
def get_dev_examples(self, data_dir):
"""See base class."""
return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
def get_labels(self):
"""See base class."""
return ["0", "1"]
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, line[0])
text_a = line[1]
text_b = line[2]
label = line[-1]
examples.append(InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
return examples
class InputExample(object):
"""A single training/test example for simple sequence classification.
Args:
guid: Unique id for the example.
text_a: The untokenized text of the first sequence.
For single sequence tasks, only this sequence must be specified.
text_b: The untokenized text of the second
sequence. Only must be specified for sequence pair tasks.
label:The label of the example. This should be
specified for train and dev examples, but not for test examples.
"""
def __init__(self, guid: int, text_a: str, text_b: str = None, label: str = None):
"""Constructs a InputExample."""
self.guid = guid
self.text_a = text_a
self.text_b = text_b
self.label = label
@@ -0,0 +1,365 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Some code of this file was adapted from the HuggingFace library available at
# https://github.com/huggingface/transformers
import os
import pickle
from typing import Dict, List, Optional, Union
import numpy as np
import torch
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.collections.nlp.data.glue_benchmark.data_processors import (
ColaProcessor,
MnliMismatchedProcessor,
MnliProcessor,
MrpcProcessor,
QnliProcessor,
QqpProcessor,
RteProcessor,
Sst2Processor,
StsbProcessor,
WnliProcessor,
)
from nemo.core.classes import Dataset
from nemo.core.neural_types import CategoricalValuesType, ChannelType, MaskType, NeuralType, RegressionValuesType
from nemo.utils import logging
__all__ = ['GLUEDataset']
processors = {
"cola": ColaProcessor,
"mnli": MnliProcessor,
"mnli-mm": MnliMismatchedProcessor,
"mrpc": MrpcProcessor,
"sst-2": Sst2Processor,
"sts-b": StsbProcessor,
"qqp": QqpProcessor,
"qnli": QnliProcessor,
"rte": RteProcessor,
"wnli": WnliProcessor,
}
output_modes = {
"cola": "classification",
"mnli": "classification",
"mnli-mm": "classification",
"mrpc": "classification",
"sst-2": "classification",
"sts-b": "regression",
"qqp": "classification",
"qnli": "classification",
"rte": "classification",
"wnli": "classification",
}
GLUE_TASKS_NUM_LABELS = {
"cola": 2,
"mnli": 3,
"mrpc": 2,
"sst-2": 2,
"sts-b": 1,
"qqp": 2,
"qnli": 2,
"rte": 2,
"wnli": 2,
}
class GLUEDataset(Dataset):
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
"labels": NeuralType(
tuple('B'), RegressionValuesType() if self.task_name == 'sts-b' else CategoricalValuesType()
),
}
def __init__(
self, file_name: str, task_name: str, tokenizer: TokenizerSpec, max_seq_length: str, use_cache: bool = True,
):
"""
Processes GLUE datasets
Args:
file_name: path to file
task_name: GLUE task name
tokenizer: such as AutoTokenizer
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
use_cache: whether to use data cache
"""
logging.info(f'Processing {file_name}')
data_dir, file_name = os.path.split(file_name)
file_name = file_name[:-4]
self.tokenizer = tokenizer
evaluate = False if 'train' in file_name else True
if task_name not in processors:
raise ValueError(f'{task_name} not supported. Choose from {processors.keys()}')
if task_name == 'mnli' and 'dev_mismatched' in file_name:
self.task_name = 'mnli-mm'
else:
self.task_name = task_name
processor = processors[self.task_name]()
output_mode = output_modes[self.task_name]
self.label_list = processor.get_labels()
self.examples = processor.get_dev_examples(data_dir) if evaluate else processor.get_train_examples(data_dir)
processor_name = type(processor).__name__
vocab_size = getattr(tokenizer, "vocab_size", 0)
cached_features_file = os.path.join(
data_dir,
"cached_{}_{}_{}_{}_{}".format(
processor_name, file_name, tokenizer.name, str(max_seq_length), str(vocab_size)
),
)
if use_cache and os.path.exists(cached_features_file):
logging.info(f"loading from {cached_features_file}")
with open(cached_features_file, "rb") as reader:
self.features = pickle.load(reader)
else:
token_params = {
'bos_token': None,
'eos_token': tokenizer.eos_token,
'pad_token': tokenizer.pad_token,
'cls_token': tokenizer.cls_token,
'sep_token_extra': tokenizer.eos_token if 'roberta' in tokenizer.name.lower() else None,
}
self.features = self.convert_examples_to_features(
self.examples, self.label_list, max_seq_length, tokenizer, output_mode, **token_params
)
master_device = not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0
if master_device:
logging.info(f'Saving train features into {cached_features_file}')
with open(cached_features_file, "wb") as writer:
pickle.dump(self.features, writer)
def __len__(self):
return len(self.features)
def __getitem__(self, idx):
feature = self.features[idx]
return (
np.array(feature.input_ids),
np.array(feature.segment_ids),
np.array(feature.input_mask, dtype=np.long),
np.array(feature.label_id),
)
def convert_examples_to_features(
self,
examples: List[str],
label_list: List[int],
max_seq_length: int,
tokenizer: TokenizerSpec,
output_mode: str,
bos_token: str = None,
eos_token: str = '[SEP]',
pad_token: str = '[PAD]',
cls_token: str = '[CLS]',
sep_token_extra: str = None,
cls_token_at_end: bool = False,
cls_token_segment_id: int = 0,
pad_token_segment_id: int = 0,
pad_on_left: bool = False,
mask_padding_with_zero: bool = True,
sequence_a_segment_id: int = 0,
sequence_b_segment_id: int = 1,
):
"""
Loads a data file into a list of `InputBatch`s.
The `cls_token_at_end` defines the location of the CLS token:
* False (Default, BERT/XLM pattern): [CLS] + A + [SEP] + B + [SEP]
* True (XLNet/GPT pattern): A + [SEP] + B + [SEP] + [CLS]
The `cls_token_segment_id` defines the segment id associated to the CLS token (0 for BERT, 2 for XLNet)
The convention in BERT is:
a. For sequence pairs:
* tokens: [CLS] is this jack ##ville ? [SEP] no it is not . [SEP]
* type_ids: 0 0 0 0 0 0 0 1 1 1 1 1 1
b. For single sequences:
* tokens: [CLS] the dog is hairy . [SEP]
* type_ids: 0 0 0 0 0 0 0
Where "type_ids" are used to indicate whether this is the first
sequence or the second sequence. The embedding vectors for `type=0`
and `type=1` were learned during pre-training and are added to the
wordpiece embedding vector (and position vector). This is
not *strictly* necessarysince the [SEP] token unambiguously separates
the sequences, but it makes it easier for the model to learn
the concept of sequences.
For classification tasks, the first vector (corresponding to [CLS])
is used as as the "sentence vector". Note that this only makes sense
because the entire model is fine-tuned.
The convention for NMT is:
a. For sequence pairs:
* tokens:<BOS> is this jack ##ville ? <EOS> <BOS> no it is not . <EOS>
* type_ids:0 0 0 0 0 0 0 1 1 1 1 1 1 1
b. For single sequences:
* tokens: <BOS> the dog is hairy . <EOS>
* type_ids: 0 0 0 0 0 0 0
"""
label_map = {label: i for i, label in enumerate(label_list)}
features = []
for ex_index, example in enumerate(examples):
if ex_index % 10000 == 0:
logging.info("Writing example %d of %d" % (ex_index, len(examples)))
tokens_a = tokenizer.text_to_tokens(example.text_a)
tokens_b = None
if example.text_b:
tokens_b = tokenizer.text_to_tokens(example.text_b)
special_tokens_count = 2 if eos_token else 0
special_tokens_count += 1 if sep_token_extra else 0
special_tokens_count += 2 if bos_token else 0
special_tokens_count += 1 if cls_token else 0
self._truncate_seq_pair(tokens_a, tokens_b, max_seq_length - special_tokens_count)
else:
special_tokens_count = 1 if eos_token else 0
special_tokens_count += 1 if sep_token_extra else 0
special_tokens_count += 1 if bos_token else 0
if len(tokens_a) > max_seq_length - special_tokens_count:
tokens_a = tokens_a[: max_seq_length - special_tokens_count]
# Add special tokens to sequence_a
tokens = tokens_a
if bos_token:
tokens = [bos_token] + tokens
if eos_token:
tokens += [eos_token]
segment_ids = [sequence_a_segment_id] * len(tokens)
# Add sequence separator between sequences
if tokens_b and sep_token_extra:
tokens += [sep_token_extra]
segment_ids += [sequence_a_segment_id]
# Add special tokens to sequence_b
if tokens_b:
if bos_token:
tokens += [bos_token]
segment_ids += [sequence_b_segment_id]
tokens += tokens_b
segment_ids += [sequence_b_segment_id] * (len(tokens_b))
if eos_token:
tokens += [eos_token]
segment_ids += [sequence_b_segment_id]
# Add classification token - for BERT models
if cls_token:
if cls_token_at_end:
tokens += [cls_token]
segment_ids += [cls_token_segment_id]
else:
tokens = [cls_token] + tokens
segment_ids = [cls_token_segment_id] + segment_ids
input_ids = tokenizer.tokens_to_ids(tokens)
# The mask has 1 for real tokens and 0 for padding tokens. Only real
# tokens are attended to.
input_mask = [1 if mask_padding_with_zero else 0] * len(input_ids)
# Zero-pad up to the sequence length.
padding_length = max_seq_length - len(input_ids)
pad_token_id = tokenizer.tokens_to_ids([pad_token])[0]
if pad_on_left:
input_ids = ([pad_token_id] * padding_length) + input_ids
input_mask = ([0 if mask_padding_with_zero else 1] * padding_length) + input_mask
segment_ids = ([pad_token_segment_id] * padding_length) + segment_ids
else:
input_ids = input_ids + ([pad_token_id] * padding_length)
input_mask = input_mask + ([0 if mask_padding_with_zero else 1] * padding_length)
segment_ids = segment_ids + ([pad_token_segment_id] * padding_length)
if len(input_ids) != max_seq_length:
raise ValueError("input_ids must be of length max_seq_length")
if len(input_mask) != max_seq_length:
raise ValueError("input_mask must be of length max_seq_length")
if len(segment_ids) != max_seq_length:
raise ValueError("segment_ids must be of length max_seq_length")
if output_mode == "classification":
label_id = label_map[example.label]
elif output_mode == "regression":
label_id = np.float32(example.label)
else:
raise KeyError(output_mode)
if ex_index < 5:
logging.info("*** Example ***")
logging.info("guid: %s" % (example.guid))
logging.info("tokens: %s" % " ".join(list(map(str, tokens))))
logging.info("input_ids: %s" % " ".join(list(map(str, input_ids))))
logging.info("input_mask: %s" % " ".join(list(map(str, input_mask))))
logging.info("segment_ids: %s" % " ".join(list(map(str, segment_ids))))
logging.info("label: %s (id = %d)" % (example.label, label_id))
features.append(
InputFeatures(input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids, label_id=label_id)
)
return features
def _truncate_seq_pair(self, tokens_a: str, tokens_b: str, max_length: int):
"""Truncates a sequence pair in place to the maximum length.
This will always truncate the longer sequence one token at a time.
This makes more sense than truncating an equal percent
of tokens from each, since if one sequence is very short then each token
that's truncated likely contains more information than a longer sequence.
"""
while True:
total_length = len(tokens_a) + len(tokens_b)
if total_length <= max_length:
break
if len(tokens_a) > len(tokens_b):
tokens_a.pop()
else:
tokens_b.pop()
class InputFeatures(object):
"""A single set of features of data.
Args:
input_ids: input/token ids
input_mask: masks out subword tokens
segment_ids: distinguish one sentence from the other one (if present)
label_ids: label for the current example
"""
def __init__(
self, input_ids: List[int], input_mask: List[int], segment_ids: List[int], label_id: Union[float, int]
):
"""Initialized InputFeatures."""
self.input_ids = input_ids
self.input_mask = input_mask
self.segment_ids = segment_ids
self.label_id = label_id
@@ -0,0 +1,17 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.information_retrieval.information_retrieval_dataset import (
BertInformationRetrievalDataset,
)
@@ -0,0 +1,278 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import multiprocessing as mp
import os
import pickle
import random
from typing import Optional
import numpy as np
from torch.utils.data import Dataset
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
__all__ = ["BertInformationRetrievalDataset"]
class BaseInformationRetrievalDataset(Dataset):
"""
Base information retrieval dataset on which other datasets are built.
Args:
tokenizer: tokenizer
max_query_length: maximum length of query in tokens
max_passage_length: maximum length of passage in tokens
"""
def __init__(
self, tokenizer: TokenizerSpec, max_query_length: Optional[int] = 31, max_passage_length: Optional[int] = 190,
):
self.tokenizer = tokenizer
self.max_query_length = max_query_length
self.max_passage_length = max_passage_length
def parse_npz(self, file, max_seq_length):
"""
Function which parses passages (documents) in npz format.
After pre-processing and tokenization, the dataset will be saved
as numpy matrix, i_th entry of which corresponds to i_th passage (document)
and has the following form:
[n, token_1, ..., token_n, 0, ..., 0]
where n is the passage length (in tokens) and 0s correspond to pad tokens.
Args:
file: str, path to file with passages (documents)
max_seq_length: maximum length of sequence in tokens
"""
cached_collection = file + ".npz"
if os.path.isfile(cached_collection):
dataset_npz = np.load(cached_collection)["data"]
else:
dataset_dict = self.tokenize_dataset(file, max_seq_length)
dataset_npz = np.zeros((len(dataset_dict), max_seq_length + 1))
for key in dataset_dict:
dataset_npz[key][0] = len(dataset_dict[key])
dataset_npz[key][1 : len(dataset_dict[key]) + 1] = dataset_dict[key]
np.savez(cached_collection, data=dataset_npz)
return dataset_npz
def parse_pkl(self, file, max_seq_length):
"""
Function which parses passages (documents, queries) in pkl format.
After pre-processing and tokenization, the dataset will be saved
as pkl dict, i_th entry of which corresponds to i_th passage (document, query)
and has the following form:
{passage_id: [token_1, ..., token_n]}
where n is the passage length (in tokens).
Args:
file: str, path to file with passages (documents)
max_seq_length: maximum length of sequence in tokens
"""
cached_collection = file + ".pkl"
if os.path.isfile(cached_collection):
dataset_dict = pickle.load(open(cached_collection, "rb"))
else:
dataset_dict = self.tokenize_dataset(file, max_seq_length)
pickle.dump(dataset_dict, open(cached_collection, "wb"))
return dataset_dict
def tokenize_dataset(self, file, max_seq_length):
"""
Function which pre-tokenizes the dataset.
"""
lines = open(file, "r").readlines()
with mp.Pool() as pool:
dataset_dict = pool.map(self.preprocess_line, lines)
dataset_dict = {id_: tokens[:max_seq_length] for (id_, tokens) in dataset_dict}
return dataset_dict
def preprocess_line(self, line):
"""
Parse a single entry (line) of tsv file.
"""
if "\t" not in line:
raise ValueError(f"Provided dataset does not have a form of tsv file")
id_, text = line.split("\t")
token_ids = self.tokenizer.text_to_ids(text.strip())
return int(id_), token_ids
def construct_input(self, token_ids1, max_seq_length, token_ids2=None):
"""
Function which constructs a valid input to BERT from tokens.
If only one list of tokens (token_ids1) is passed, the input will be
[CLS] token_ids1 [SEP]
if two lists of tokens are passed, the input will be
[CLS] token_ids1 [SEP] token_ids2 [SEP]
"""
input_ids = [self.tokenizer.pad_id] * max_seq_length
bert_input = [self.tokenizer.cls_id] + token_ids1 + [self.tokenizer.sep_id]
sentence1_length = len(bert_input)
if token_ids2 is not None:
bert_input = bert_input + token_ids2 + [self.tokenizer.sep_id]
bert_input = bert_input[:max_seq_length]
num_nonpad_tokens = len(bert_input)
input_ids[:num_nonpad_tokens] = bert_input
input_ids = np.array(input_ids, dtype=np.long)
input_mask = input_ids != self.tokenizer.pad_id
input_type_ids = np.ones_like(input_ids)
input_type_ids[:sentence1_length] = 0
return input_ids, input_mask, input_type_ids
def preprocess_bert(self, query_id, psg_ids):
"""
Transforms query id (Q) and a list of passages ids (P1, ..., Pk)
into a tensor of size [k, max_length] with the following rows:
[CLS] Q_text [SEP] Pi_text [SEP], i = 1, ..., k
"""
max_seq_length = self.max_query_length + self.max_passage_length + 3
input_ids, input_mask, input_type_ids = [], [], []
for psg_id in psg_ids:
inputs = self.construct_input(self.queries[query_id], max_seq_length, self._psgid2tokens(psg_id))
input_ids.append(inputs[0])
input_mask.append(inputs[1])
input_type_ids.append(inputs[2])
input_ids = np.stack(input_ids)
input_mask = np.stack(input_mask)
input_type_ids = np.stack(input_type_ids)
return input_ids, input_mask, input_type_ids
def preprocess_dpr(self, query_id, psg_ids):
"""
Transforms query id (Q) and a list of passages ids (P1, ..., Pk)
into two tensors of sizes [1, max_q_length] and [k, max_p_length]
with the following rows:
1) [CLS] Q_text [SEP]
2) [CLS] Pi_text [SEP], i = 1, ..., k
"""
q_input_ids, q_input_mask, q_type_ids = self.construct_input(self.queries[query_id], self.max_query_length + 2)
input_ids, input_mask, input_type_ids = [], [], []
for psg_id in psg_ids:
inputs = self.construct_input(self._psgid2tokens(psg_id), self.max_passage_length + 2)
input_ids.append(inputs[0])
input_mask.append(inputs[1])
input_type_ids.append(inputs[2])
input_ids = np.stack(input_ids)
input_mask = np.stack(input_mask)
input_type_ids = np.stack(input_type_ids)
return (
q_input_ids[None, ...],
q_input_mask[None, ...],
q_type_ids[None, ...],
input_ids,
input_mask,
input_type_ids,
)
def _psgid2tokens(self, psg_id):
"""
Internal function which maps passage id to its tokens.
"""
pass
def psgid2tokens_npz(self, psg_id):
"""
Mapping from passage id to its tokens in case of npz cache format.
"""
seq_len = self.passages[psg_id][0]
return self.passages[psg_id][1 : seq_len + 1].tolist()
def psgid2tokens_pkl(self, psg_id):
"""
Mapping from passage id to its tokens in case of pkl cache format.
"""
return self.passages[psg_id]
class BertInformationRetrievalDataset(BaseInformationRetrievalDataset):
def __init__(
self,
tokenizer: TokenizerSpec,
passages: str,
queries: str,
query_to_passages: str,
max_query_length: Optional[int] = 31,
max_passage_length: Optional[int] = 190,
num_negatives: Optional[int] = 10,
preprocess_fn: Optional[str] = "preprocess_bert",
psg_cache_format: Optional[str] = "npz",
):
"""
Dataset for training information retrieval models.
Args:
tokenizer: tokenizer
passages: path to tsv with [psg_id, psg_text] entries
queries: path to tsv with [query_id, query_text] entries
query_to_passages: path to tsv with
[query_id, pos_psg_id, neg_psg_id_1, ..., neg_psg_id_k] entries
max_query_length: maximum length of query in tokens
max_passage_length: maximum length of passage in tokens
num_negatives: number of negative passages per positive to use for training
preprocess_fn: either preprocess_bert or preprocess_dpr
preprocess_bert: joint input: [CLS] query [SEP] passage [SEP]
preprocess_dpr: separate inputs: [CLS] query [SEP], [CLS] passage [SEP]
psg_cache_format: either pkl or npz
"""
super().__init__(tokenizer, max_query_length, max_passage_length)
self.num_negatives = num_negatives
self.passages = getattr(self, f"parse_{psg_cache_format}")(passages, max_passage_length)
self._psgid2tokens = getattr(self, f"psgid2tokens_{psg_cache_format}")
self.queries = self.parse_pkl(queries, max_query_length)
self.idx2psgs = self.parse_query_to_passages(query_to_passages)
self._preprocess_fn = getattr(self, preprocess_fn)
def __getitem__(self, idx):
query_and_psgs = self.idx2psgs[idx]
query_id, psg_ids = query_and_psgs[0], query_and_psgs[1:]
inputs = self._preprocess_fn(query_id, psg_ids)
return [*inputs, query_id, np.array(psg_ids)]
def __len__(self):
return len(self.idx2psgs)
def parse_query_to_passages(self, file):
"""
Function which parses query to passages correspondence file.
"""
idx2psgs = {}
idx = 0
for line in open(file, "r").readlines():
if "\t" not in line:
raise ValueError(f"Provided dataset does not have a form of tsv file")
query_and_psgs = line.split("\t")
query_and_psgs_ids = [int(id_) for id_ in query_and_psgs]
query_and_rel_psg_ids, irrel_psgs_ids = query_and_psgs_ids[:2], query_and_psgs_ids[2:]
random.shuffle(irrel_psgs_ids)
num_samples = len(irrel_psgs_ids) // self.num_negatives
for j in range(num_samples):
left = self.num_negatives * j
right = self.num_negatives * (j + 1)
idx2psgs[idx] = query_and_rel_psg_ids + irrel_psgs_ids[left:right]
idx += 1
return idx2psgs
@@ -0,0 +1,22 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.intent_slot_classification.intent_slot_classification_dataset import (
IntentSlotClassificationDataset,
IntentSlotInferenceDataset,
)
from nemo.collections.nlp.data.intent_slot_classification.intent_slot_classification_descriptor import (
IntentSlotDataDesc,
)
@@ -0,0 +1,292 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Dict, Optional
import numpy as np
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.collections.nlp.data.data_utils import get_stats
from nemo.core.classes import Dataset
from nemo.core.neural_types import ChannelType, LabelsType, MaskType, NeuralType
from nemo.utils import logging
__all__ = ['IntentSlotClassificationDataset', 'IntentSlotInferenceDataset']
def get_features(
queries,
max_seq_length,
tokenizer,
pad_label=128,
raw_slots=None,
ignore_extra_tokens=False,
ignore_start_end=False,
):
all_subtokens = []
all_loss_mask = []
all_subtokens_mask = []
all_segment_ids = []
all_input_ids = []
all_input_mask = []
sent_lengths = []
all_slots = []
with_label = False
if raw_slots is not None:
with_label = True
for i, query in enumerate(queries):
words = query.strip().split()
subtokens = [tokenizer.cls_token]
loss_mask = [1 - ignore_start_end]
subtokens_mask = [0]
if with_label:
slots = [pad_label]
for j, word in enumerate(words):
word_tokens = tokenizer.text_to_tokens(word)
subtokens.extend(word_tokens)
loss_mask.append(1)
loss_mask.extend([int(not ignore_extra_tokens)] * (len(word_tokens) - 1))
subtokens_mask.append(1)
subtokens_mask.extend([0] * (len(word_tokens) - 1))
if with_label:
slots.extend([raw_slots[i][j]] * len(word_tokens))
subtokens.append(tokenizer.sep_token)
loss_mask.append(1 - ignore_start_end)
subtokens_mask.append(0)
sent_lengths.append(len(subtokens))
all_subtokens.append(subtokens)
all_loss_mask.append(loss_mask)
all_subtokens_mask.append(subtokens_mask)
all_input_mask.append([1] * len(subtokens))
if with_label:
slots.append(pad_label)
all_slots.append(slots)
max_seq_length_data = max(sent_lengths)
max_seq_length = min(max_seq_length, max_seq_length_data) if max_seq_length > 0 else max_seq_length_data
logging.info(f'Setting max length to: {max_seq_length}')
get_stats(sent_lengths)
too_long_count = 0
for i, subtokens in enumerate(all_subtokens):
if len(subtokens) > max_seq_length:
subtokens = [tokenizer.cls_token] + subtokens[-max_seq_length + 1 :]
all_input_mask[i] = [1] + all_input_mask[i][-max_seq_length + 1 :]
all_loss_mask[i] = [1 - ignore_start_end] + all_loss_mask[i][-max_seq_length + 1 :]
all_subtokens_mask[i] = [0] + all_subtokens_mask[i][-max_seq_length + 1 :]
if with_label:
all_slots[i] = [pad_label] + all_slots[i][-max_seq_length + 1 :]
too_long_count += 1
all_input_ids.append([tokenizer.tokens_to_ids(t) for t in subtokens])
if len(subtokens) < max_seq_length:
extra = max_seq_length - len(subtokens)
all_input_ids[i] = all_input_ids[i] + [0] * extra
all_loss_mask[i] = all_loss_mask[i] + [0] * extra
all_subtokens_mask[i] = all_subtokens_mask[i] + [0] * extra
all_input_mask[i] = all_input_mask[i] + [0] * extra
if with_label:
all_slots[i] = all_slots[i] + [pad_label] * extra
all_segment_ids.append([0] * max_seq_length)
logging.info(f'{too_long_count} are longer than {max_seq_length}')
# May be useful for debugging
logging.debug("*** Some Examples of Processed Data ***")
for i in range(min(len(all_input_ids), 5)):
logging.debug("i: %s" % (i))
logging.debug("subtokens: %s" % " ".join(list(map(str, all_subtokens[i]))))
logging.debug("loss_mask: %s" % " ".join(list(map(str, all_loss_mask[i]))))
logging.debug("input_mask: %s" % " ".join(list(map(str, all_input_mask[i]))))
logging.debug("subtokens_mask: %s" % " ".join(list(map(str, all_subtokens_mask[i]))))
if with_label:
logging.debug("slots_label: %s" % " ".join(list(map(str, all_slots[i]))))
return (all_input_ids, all_segment_ids, all_input_mask, all_loss_mask, all_subtokens_mask, all_slots)
class IntentSlotClassificationDataset(Dataset):
"""
Creates dataset to use for the task of joint intent
and slot classification with pretrained model.
Converts from raw data to an instance that can be used by
NMDataLayer.
For dataset to use during inference without labels, see
IntentSlotDataset.
Args:
input_file: file to sequence + label. the first line is header (sentence [tab] label)
each line should be [sentence][tab][label]
slot_file: file to slot labels, each line corresponding to slot labels for a sentence in input_file. No header.
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
tokenizer: such as NemoBertTokenizer
num_samples: number of samples you want to use for the dataset. If -1, use all dataset. Useful for testing.
pad_label: pad value use for slot labels. by default, it's the neutral label.
ignore_extra_tokens: whether to ignore extra tokens in the loss_mask.
ignore_start_end: whether to ignore bos and eos tokens in the loss_mask.
do_lower_case: convert query to lower case or not
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
'loss_mask': NeuralType(('B', 'T'), MaskType()),
'subtokens_mask': NeuralType(('B', 'T'), MaskType()),
'intent_labels': NeuralType(('B'), LabelsType()),
'slot_labels': NeuralType(('B', 'T'), LabelsType()),
}
def __init__(
self,
input_file: str,
slot_file: str,
max_seq_length: int,
tokenizer: TokenizerSpec,
num_samples: int = -1,
pad_label: int = 128,
ignore_extra_tokens: bool = False,
ignore_start_end: bool = False,
do_lower_case: bool = False,
):
if num_samples == 0:
raise ValueError("num_samples has to be positive", num_samples)
with open(slot_file, 'r') as f:
slot_lines = f.readlines()
with open(input_file, 'r') as f:
input_lines = f.readlines()[1:]
assert len(slot_lines) == len(input_lines)
dataset = list(zip(slot_lines, input_lines))
if num_samples > 0:
dataset = dataset[:num_samples]
raw_slots, queries, raw_intents = [], [], []
for slot_line, input_line in dataset:
raw_slots.append([int(slot) for slot in slot_line.strip().split()])
parts = input_line.strip().split()
raw_intents.append(int(parts[-1]))
query = ' '.join(parts[:-1])
if do_lower_case:
query = query.lower()
queries.append(query)
features = get_features(
queries,
max_seq_length,
tokenizer,
pad_label=pad_label,
raw_slots=raw_slots,
ignore_extra_tokens=ignore_extra_tokens,
ignore_start_end=ignore_start_end,
)
self.all_input_ids = features[0]
self.all_segment_ids = features[1]
self.all_input_mask = features[2]
self.all_loss_mask = features[3]
self.all_subtokens_mask = features[4]
self.all_slots = features[5]
self.all_intents = raw_intents
def __len__(self):
return len(self.all_input_ids)
def __getitem__(self, idx):
return (
np.array(self.all_input_ids[idx]),
np.array(self.all_segment_ids[idx]),
np.array(self.all_input_mask[idx], dtype=np.long),
np.array(self.all_loss_mask[idx]),
np.array(self.all_subtokens_mask[idx]),
self.all_intents[idx],
np.array(self.all_slots[idx]),
)
class IntentSlotInferenceDataset(Dataset):
"""
Creates dataset to use for the task of joint intent
and slot classification with pretrained model.
This is to be used during inference only.
It uses list of queries as the input.
Args:
queries (list): list of queries to run inference on
max_seq_length (int): max sequence length minus 2 for [CLS] and [SEP]
tokenizer (Tokenizer): such as NemoBertTokenizer
pad_label (int): pad value use for slot labels.
by default, it's the neutral label.
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""
Returns definitions of module output ports.
"""
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
'loss_mask': NeuralType(('B', 'T'), MaskType()),
'subtokens_mask': NeuralType(('B', 'T'), MaskType()),
}
def __init__(self, queries, max_seq_length, tokenizer, do_lower_case):
if do_lower_case:
for idx, query in enumerate(queries):
queries[idx] = queries[idx].lower()
features = get_features(queries, max_seq_length, tokenizer)
self.all_input_ids = features[0]
self.all_segment_ids = features[1]
self.all_input_mask = features[2]
self.all_loss_mask = features[3]
self.all_subtokens_mask = features[4]
def __len__(self):
return len(self.all_input_ids)
def __getitem__(self, idx):
return (
np.array(self.all_input_ids[idx]),
np.array(self.all_segment_ids[idx]),
np.array(self.all_input_mask[idx], dtype=np.long),
np.array(self.all_loss_mask[idx]),
np.array(self.all_subtokens_mask[idx]),
)
@@ -0,0 +1,163 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import itertools
from typing import List
from nemo.collections.nlp.data.data_utils.data_preprocessing import (
fill_class_weights,
get_freq_weights,
get_label_stats,
if_exist,
)
from nemo.utils import logging
class IntentSlotDataDesc:
""" Convert the raw data to the standard format supported by
IntentSlotDataDesc.
By default, the None label for slots is 'O'.
IntentSlotDataDesc requires two files:
input_file: file to sequence + label.
the first line is header (sentence [tab] label)
each line should be [sentence][tab][label]
slot_file: file to slot labels, each line corresponding to
slot labels for a sentence in input_file. No header.
To keep the mapping from label index to label consistent during
training and inferencing we require the following files:
dicts.intents.csv: each line is an intent. The first line
corresponding to the 0 intent label, the second line
corresponding to the 1 intent label, and so on.
dicts.slots.csv: each line is a slot. The first line
corresponding to the 0 slot label, the second line
corresponding to the 1 slot label, and so on.
Args:
data_dir: the directory of the dataset
modes: ['train', 'test', 'dev'],
none_slot_label: the label for slots that aren't identified defaulted to 'O'
pad_label: the int used for padding. If set to -1, it'll be set to the whatever the None label is.
"""
def __init__(
self,
data_dir: str,
modes: List[str] = ['train', 'test', 'dev'],
none_slot_label: str = 'O',
pad_label: int = -1,
):
if not if_exist(data_dir, ['dict.intents.csv', 'dict.slots.csv']):
raise FileNotFoundError(
"Make sure that your data follows the standard format "
"supported by JointIntentSlotDataset. Your data must "
"contain dict.intents.csv and dict.slots.csv."
)
self.data_dir = data_dir
self.intent_dict_file = self.data_dir + '/dict.intents.csv'
self.slot_dict_file = self.data_dir + '/dict.slots.csv'
self.intents_label_ids = IntentSlotDataDesc.label2idx(self.intent_dict_file)
self.num_intents = len(self.intents_label_ids)
self.slots_label_ids = IntentSlotDataDesc.label2idx(self.slot_dict_file)
self.num_slots = len(self.slots_label_ids)
infold = self.data_dir
for mode in modes:
if not if_exist(self.data_dir, [f'{mode}.tsv']):
logging.info(f' Stats calculation for {mode} mode' f' is skipped as {mode}.tsv was not found.')
continue
logging.info(f' Stats calculating for {mode} mode...')
slot_file = f'{self.data_dir}/{mode}_slots.tsv'
with open(slot_file, 'r') as f:
slot_lines = f.readlines()
input_file = f'{self.data_dir}/{mode}.tsv'
with open(input_file, 'r') as f:
input_lines = f.readlines()[1:] # Skipping headers at index 0
if len(slot_lines) != len(input_lines):
raise ValueError(
"Make sure that the number of slot lines match the "
"number of intent lines. There should be a 1-1 "
"correspondence between every slot and intent lines."
)
dataset = list(zip(slot_lines, input_lines))
raw_slots, raw_intents = [], []
for slot_line, input_line in dataset:
slot_list = [int(slot) for slot in slot_line.strip().split()]
raw_slots.append(slot_list)
parts = input_line.strip().split()
raw_intents.append(int(parts[-1]))
logging.info(f'Three most popular intents in {mode} mode:')
total_intents, intent_label_freq, max_id = get_label_stats(
raw_intents, infold + f'/{mode}_intent_stats.tsv'
)
merged_slots = itertools.chain.from_iterable(raw_slots)
logging.info(f'Three most popular slots in {mode} mode:')
slots_total, slots_label_freq, max_id = get_label_stats(merged_slots, infold + f'/{mode}_slot_stats.tsv')
logging.info(f'Total Number of Intents: {total_intents}')
logging.info(f'Intent Label Frequencies: {intent_label_freq}')
logging.info(f'Total Number of Slots: {slots_total}')
logging.info(f'Slots Label Frequencies: {slots_label_freq}')
if mode == 'train':
intent_weights_dict = get_freq_weights(intent_label_freq)
logging.info(f'Intent Weights: {intent_weights_dict}')
slot_weights_dict = get_freq_weights(slots_label_freq)
logging.info(f'Slot Weights: {slot_weights_dict}')
self.intent_weights = fill_class_weights(intent_weights_dict, self.num_intents - 1)
self.slot_weights = fill_class_weights(slot_weights_dict, self.num_slots - 1)
if pad_label != -1:
self.pad_label = pad_label
else:
if none_slot_label not in self.slots_label_ids:
raise ValueError(f'none_slot_label {none_slot_label} not ' f'found in {self.slot_dict_file}.')
self.pad_label = self.slots_label_ids[none_slot_label]
@staticmethod
def label2idx(file):
lines = open(file, 'r').readlines()
lines = [line.strip() for line in lines if line.strip()]
labels = {lines[i]: i for i in range(len(lines))}
return labels
@staticmethod
def intent_slot_dicts(data_dir):
'''
Return Intent and slot dictionaries
'''
intent_dict_file = data_dir + '/dict.intents.csv'
slot_dict_file = data_dir + '/dict.slots.csv'
intents_labels = open(intent_dict_file, 'r').readlines()
intents_labels = [line.strip() for line in intents_labels if line.strip()]
slots_labels = open(slot_dict_file, 'r').readlines()
slots_labels = [line.strip() for line in slots_labels if line.strip()]
return intents_labels, slots_labels
@@ -0,0 +1,20 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.language_modeling.l2r_lm_dataset import L2RLanguageModelingDataset
from nemo.collections.nlp.data.language_modeling.lm_bert_dataset import (
BertPretrainingDataset,
BertPretrainingPreprocessedDataloader,
)
from nemo.collections.nlp.data.language_modeling.sentence_dataset import SentenceDataset, TarredSentenceDataset
@@ -0,0 +1,240 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import json
from typing import Optional
import braceexpand
import numpy as np
import webdataset as wd
from torch.utils.data import Dataset, IterableDataset
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.collections.nlp.data.data_utils import dataset_to_ids
from nemo.utils import logging
__all__ = ['L2RLanguageModelingDataset', 'TarredL2RLanguageModelingDataset']
class L2RLanguageModelingDataset(Dataset):
"""
Dataset for training and evaluating left-to-right language models.
Args:
tokenizer: tokenizer, such as WordTokenizer or CharTokenizer
dataset: path to data
max_seq_length: maximum sequence length (in tokens) of input tensors
batch_step: distance (in tokens) between two successive sequences of
the text. By default, it is equal to max_seq_length which corresponds
to splitting text into disjoint segments covering full dataset
use_cache: bool value, defaults to False. Determines whether the preprocessed,
tokenized dataset should be cached into a pickle file. If true, cache is saved
at the path provided in `dataset`.
"""
def __init__(
self,
tokenizer: TokenizerSpec,
dataset: str,
max_seq_length: Optional[int] = 512,
batch_step: Optional[int] = None,
use_cache: bool = False,
):
self.tokenizer = tokenizer
self.max_seq_length = max_seq_length
self.batch_step = batch_step or self.max_seq_length
ids = dataset_to_ids(dataset, tokenizer, cache_ids=use_cache, add_bos_eos=False)
self.ids = np.array([j for i in ids for j in i])
def __len__(self):
return (len(self.ids) - self.max_seq_length) // self.batch_step
def __getitem__(self, idx):
left = idx * self.batch_step
right = left + self.max_seq_length
src_ids = self.ids[left:right]
labels = self.ids[left + 1 : right + 1]
src_mask = (src_ids != self.tokenizer.pad_id).astype(np.float32)
return src_ids, src_mask, labels
class TarredL2RLanguageModelingDataset(IterableDataset):
"""
A similar Dataset to the L2RLanguageModelingDataset, but which loads tarred tokenized numpy files.
Accepts a single JSON metadata manifest file as well as the path(s) to the tarball(s) containing the wav files.
The manifest should contain information such as the number of shards, the number of tokens in the corpus,
and the number of tokens contained within each shard of the tarfile(s).
Valid formats for the text_tar_filepaths argument include:
(1) a single string that can be brace-expanded, e.g. 'path/to/text.tar' or 'path/to/text_{1..100}.tar.gz', or
(2) a list of file paths that will not be brace-expanded, e.g. ['text_1.tar', 'text_2.tar', ...].
Note: For brace expansion in (1), there may be cases where `{x..y}` syntax cannot be used due to shell interference.
This occurs most commonly inside SLURM scripts. Therefore we provide a few equivalent replacements.
Supported opening braces - { <=> (, [, < and the special tag _OP_.
Supported closing braces - } <=> ), ], > and the special tag _CL_.
For SLURM based tasks, we suggest the use of the special tags for ease of use.
See the WebDataset documentation for more information about accepted data and input formats.
If using multiple processes the number of shards should be divisible by the number of workers to ensure an
even split among workers. If it is not divisible, logging will give a warning but training will proceed.
Additionally, please note that the len() of this DataLayer is assumed to be the number of tokens
of the text data. An incorrect manifest length may lead to some DataLoader issues down the line.
Args:
text_tar_filepaths: Either a list of tokenized text tarball filepaths, or a
string (can be brace-expandable).
metadata_path (str): Path to the metadata manifest.
tokenizer: tokenizer, such as WordTokenizer or CharTokenizer
dataset: path to data
max_seq_length: maximum sequence length (in tokens) of input tensors
batch_step: distance (in tokens) between two successive sequences of
the text. By default, it is equal to max_seq_length which corresponds
to splitting text into disjoint segments covering full dataset
shuffle_n (int): How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
Defaults to 0.
shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp.
- `scatter`: The default shard strategy applied by WebDataset, where each node gets
a unique set of shards, which are permanently pre-allocated and never changed at runtime.
- `replicate`: Optional shard strategy, where each node gets all of the set of shards
available in the tarred dataset, which are permanently pre-allocated and never changed at runtime.
The benefit of replication is that it allows each node to sample data points from the entire
dataset independently of other nodes, and reduces dependence on value of `shuffle_n`.
Note: Replicated strategy allows every node to sample the entire set of available tarfiles,
and therefore more than one node may sample the same tarfile, and even sample the same
data points! As such, there is no assured guarantee that all samples in the dataset will be
sampled at least once during 1 epoch.
global_rank (int): Worker rank, used for partitioning shards. Defaults to 0.
world_size (int): Total number of processes, used for partitioning shards. Defaults to 0.
"""
def __init__(
self,
text_tar_filepaths: str,
metadata_path: str,
tokenizer,
max_seq_length: int = 512,
batch_step: int = None,
shuffle_n: int = 1,
shard_strategy: str = "scatter",
global_rank: int = 0,
world_size: int = 0,
):
super(TarredL2RLanguageModelingDataset, self).__init__()
self.tokenizer = tokenizer
self.max_seq_length = max_seq_length
self.batch_step = batch_step or self.max_seq_length
valid_shard_strategies = ['scatter', 'replicate']
if shard_strategy not in valid_shard_strategies:
raise ValueError(f"`shard_strategy` must be one of {valid_shard_strategies}")
with open(metadata_path, 'r') as f:
metadata = json.load(f)
self.metadata = metadata
if isinstance(text_tar_filepaths, str):
# Replace '(', '[', '<' and '_OP_' with '{'
brace_keys_open = ['(', '[', '<', '_OP_']
for bkey in brace_keys_open:
if bkey in text_tar_filepaths:
text_tar_filepaths = text_tar_filepaths.replace(bkey, "{")
# Replace ')', ']', '>' and '_CL_' with '}'
brace_keys_close = [')', ']', '>', '_CL_']
for bkey in brace_keys_close:
if bkey in text_tar_filepaths:
text_tar_filepaths = text_tar_filepaths.replace(bkey, "}")
if isinstance(text_tar_filepaths, str):
# Brace expand
text_tar_filepaths = list(braceexpand.braceexpand(text_tar_filepaths))
if shard_strategy == 'scatter':
logging.info("All tarred dataset shards will be scattered evenly across all nodes.")
if len(text_tar_filepaths) % world_size != 0:
logging.warning(
f"Number of shards in tarred dataset ({len(text_tar_filepaths)}) is not divisible "
f"by number of distributed workers ({world_size})."
)
begin_idx = (len(text_tar_filepaths) // world_size) * global_rank
end_idx = begin_idx + (len(text_tar_filepaths) // world_size)
text_tar_filepaths = text_tar_filepaths[begin_idx:end_idx]
logging.info(
"Partitioning tarred dataset: process (%d) taking shards [%d, %d)", global_rank, begin_idx, end_idx
)
elif shard_strategy == 'replicate':
logging.info("All tarred dataset shards will be replicated across all nodes.")
else:
raise ValueError(f"Invalid shard strategy ! Allowed values are : {valid_shard_strategies}")
self.tarpath = text_tar_filepaths
# Put together WebDataset
self._dataset = wd.WebDataset(urls=text_tar_filepaths, nodesplitter=None)
if shuffle_n > 0:
self._dataset = self._dataset.shuffle(shuffle_n)
else:
logging.info("WebDataset will not shuffle files within the tar files.")
self._dataset = self._dataset.rename(npy='npy', key='__key__').to_tuple('npy', 'key').map(f=self._build_sample)
def _build_sample(self, tup):
# Load file
npy, filepath = tup
npy = io.BytesIO(npy)
data = np.load(npy) # loads np.int64 vector
npy.close()
# Select random contiguous subsegment
idx = np.random.randint(0, (len(data) - self.max_seq_length) // self.batch_step)
# Slice of data chunk
left = idx * self.batch_step
right = left + self.max_seq_length
data = data[left : right + 1]
# Create batch
src_ids = data[:-1]
labels = data[1:]
src_mask = (src_ids != self.tokenizer.pad_id).astype(np.float32)
return src_ids, src_mask, labels
def __iter__(self):
# We need to wrap an infinite generator since the actual files
# within the tar files contains large chunks of contiguous data.
# This prevents PTL from early exiting the train loop after exhausting
# all of the files in one iteration (though the actual dataset is many
# times larger due to each file containing a large chunk of data).
dl_iter = iter(self._dataset)
while True:
try:
batch = next(dl_iter)
yield batch
except StopIteration:
dl_iter = iter(self._dataset)
continue
def __len__(self):
return (self.metadata['num_text'] - self.max_seq_length) // self.batch_step
@@ -0,0 +1,406 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import array
import os
import pickle
import random
from typing import Dict, List, Optional
import h5py
import numpy as np
from torch.utils.data import DataLoader, DistributedSampler
from tqdm import tqdm
from nemo.collections.nlp.data.data_utils.data_preprocessing import find_newlines, load_data_indices
from nemo.core.classes import Dataset
__all__ = ['BertPretrainingDataset', 'BertPretrainingPreprocessedDataloader']
def load_h5(input_file: str):
return h5py.File(input_file, "r")
class BertPretrainingDataset(Dataset):
"""
Dataset for bert pretraining when using data preprocessing including tokenization
"""
def __init__(
self,
tokenizer: object,
data_file: str,
max_seq_length: Optional[int] = 128,
mask_prob: Optional[float] = 0.15,
short_seq_prob: Optional[float] = 0.1,
seq_a_ratio: Optional[float] = 0.6,
sentence_idx_file: Optional[str] = None,
):
"""
Args:
tokenizer: tokenizer
data_file: path to data
max_seq_length: maximum sequence length of input tensors
mask_probability: proability to mask token
short_seq_prob: probability to create a sequence shorter than max_seq_length
seq_a_ratio: ratio between lengths of first and second sequence
sentence_idx_file: sentence indices file for caching
"""
self.tokenizer = tokenizer
# Loading enormous datasets into RAM isn't always feasible -- for
# example, the pubmed corpus is 200+ GB, which doesn't fit into RAM on
# most computers. To get around this, we store the indices of newlines
# in each file so we can seek to and retrieve sentences immediately
# from main memory when needed during training.
# Try and load sentence indices file if already exists
sentence_indices, sentence_idx_file, data_dir = load_data_indices(
sentence_idx_file, data_file, "sentence_indices"
)
# If sentence indices file doesn't exists, generate and store sentence indices
if sentence_indices is None:
sentence_indices = {}
filenames = [data_file]
for filename in tqdm(filenames):
with open(filename, "rb") as f:
contents = f.read()
newline_indices = find_newlines(contents)
if os.path.isdir(data_dir):
# Only keep the parts of the filepath that are invariant to
# the dataset's location on disk
filename = os.path.basename(filename)
# In python, arrays are much more space-efficient than lists
sentence_indices[filename] = array.array("I", newline_indices)
# Save sentence indices so we don't have to do this again
with open(sentence_idx_file, "wb") as f:
pickle.dump(sentence_indices, f)
corpus_size = 0
empty_files = []
# Find total number of newlines across entire corpus and remove files
# without any newlines
for filename in sentence_indices:
if len(sentence_indices[filename]) <= 1:
empty_files.append(filename)
else:
corpus_size += len(sentence_indices[filename])
for filename in empty_files:
del sentence_indices[filename]
self.corpus_size = corpus_size
self.dataset = data_dir
self.filenames = list(sentence_indices.keys())
self.mask_probability = mask_prob
self.max_seq_length = max_seq_length
self.sentence_indices = sentence_indices
self.vocab_size = self.tokenizer.vocab_size
self.short_seq_prob = short_seq_prob
self.seq_a_ratio = seq_a_ratio
def __len__(self):
return self.corpus_size
def __getitem__(self, idx: int, min_doc_length: Optional[int] = 16):
# Each sequence has three special tokens, as follows:
# tokenizer.cls_token <document a> tokenizer.sep_token <document b> tokenizer.eos_token
num_special_tokens = 3
max_num_tokens = self.max_seq_length - num_special_tokens
target_seq_length = max_num_tokens
if random.random() < self.short_seq_prob:
# TODO: maybe introduce an argument to control this.
target_seq_length = random.randint(2, max_num_tokens)
# prefer the seq_a to be slightly longer than seq_b, 0.6 by default
target_seq_length_a = int(round(target_seq_length * self.seq_a_ratio))
target_seq_length_b = target_seq_length - target_seq_length_a
def get_document(filepath, offset):
# Retrieve a specific line from a file and return as a document
if os.path.isdir(self.dataset):
filepath = os.path.join(self.dataset, filepath)
with open(filepath, "rb") as f:
f.seek(offset)
doc_text = f.readline()[:-1].decode("utf-8", errors="ignore")
document = self.tokenizer.text_to_ids(doc_text)
return document
def match_target_seq_length(
document: str, target_seq_length: int, filename: str, line_idx: int, sentence_indices: Dict[str, dict]
):
# If document is shorter than target sequence length,
# append the next line or take a random line as replacement.
num_lines = len(sentence_indices[filename])
while len(document) < target_seq_length:
if line_idx < (num_lines - 1):
# append the next line
line_idx += 1
else:
# current line is the last line, take a random one
line_idx = random.randrange(num_lines)
document = []
offset = sentence_indices[filename][line_idx]
document += get_document(filename, offset)
return document, line_idx
# Take sequence A from a random file and a random line
a_filename = random.choice(self.filenames)
a_line_idx = random.randrange(len(self.sentence_indices[a_filename]))
a_line_offset = self.sentence_indices[a_filename][a_line_idx]
a_document = get_document(a_filename, a_line_offset)
a_document, a_line_idx = match_target_seq_length(
a_document, target_seq_length_a, a_filename, a_line_idx, self.sentence_indices
)
is_last_line = a_line_idx >= (len(self.sentence_indices[a_filename]) - 1)
# About 50% of the time, B is a random sentence from the corpus
take_random_b = (random.random() < 0.5) or is_last_line
if take_random_b:
# This should rarely go for more than one iteration for large
# corpora. However, just to be careful, we try to make sure that
# the random document is not the same as the document
# we're processing.
for _ in range(10):
b_filename = random.choice(self.filenames)
b_line_idx = random.choice(range(len(self.sentence_indices[b_filename])))
if b_filename != a_filename:
break
else:
# Take another line from the same file
b_line_pos = self.sentence_indices[b_filename][b_line_idx]
a_line_pos = self.sentence_indices[a_filename][a_line_idx]
# TODO unclear about the following check
if abs(b_line_pos - a_line_pos) > max_num_tokens:
break
else:
pass
else:
b_filename = a_filename
b_line_idx = a_line_idx + 1
is_next = int(not take_random_b)
b_line_pos = self.sentence_indices[b_filename][b_line_idx]
b_document = get_document(b_filename, b_line_pos)
b_document, b_line_idx = match_target_seq_length(
b_document, target_seq_length_b, b_filename, b_line_idx, self.sentence_indices
)
def truncate_seq_pair(a, b, max_num_tokens):
# Truncates a pair of sequences to a maximum sequence length
while (len(a) + len(b)) > max_num_tokens:
# Truncate the longer sequence
if len(a) > len(b):
trunc_document = a
else:
trunc_document = b
if len(trunc_document) <= 1:
raise ValueError(
"Input text corpora probably too small. "
"Failed to truncate sequence pair to "
"maximum sequence legnth."
)
# Randomly truncate from the front or the back
if random.random() < 0.5:
del trunc_document[0]
else:
trunc_document.pop()
truncate_seq_pair(a_document, b_document, max_num_tokens)
output_ids = (
[self.tokenizer.cls_id] + a_document + [self.tokenizer.sep_id] + b_document + [self.tokenizer.eos_id]
)
input_ids, output_mask = self.mask_ids(output_ids)
input_mask = np.zeros(self.max_seq_length, dtype=np.long)
input_mask[: len(input_ids)] = 1
input_type_ids = np.zeros(self.max_seq_length, dtype=np.int)
input_type_ids[len(a_document) + 2 : len(output_ids) + 1] = 1
padding_length = max(0, self.max_seq_length - len(input_ids))
if padding_length > 0:
input_ids.extend([self.tokenizer.pad_id] * padding_length)
output_ids.extend([self.tokenizer.pad_id] * padding_length)
output_mask.extend([0] * padding_length)
# TODO: wrap the return value with () for consistent style.
return (
np.array(input_ids),
input_type_ids,
np.array(input_mask, dtype=np.long),
np.array(output_ids),
np.array(output_mask, dtype=np.float32),
is_next,
)
def mask_ids(self, ids: List[int]):
"""
Args:
ids: list of token ids representing a chunk of text
Returns:
masked_ids: list of input tokens with some of the entries masked
according to the following protocol from the original BERT paper:
each token is masked with a probability of 15% and is replaced with
1) the [MASK] token 80% of the time,
2) random token 10% of the time,
3) the same token 10% of the time.
output_mask: list of binary variables which indicate what tokens has
been masked (to calculate the loss function for these tokens only)
"""
# Whole-word masking by default, as it gives better performance.
cand_indexes = [[ids[0]]]
for tid in ids[1:]:
token = self.tokenizer.ids_to_tokens([tid])[0]
is_suffix = token.startswith('\u2581')
if is_suffix:
# group together with its previous token to form a whole-word
cand_indexes[-1].append(tid)
else:
cand_indexes.append([tid])
masked_ids, output_mask = [], []
mask_id = self.tokenizer.token_to_id("[MASK]")
for word_ids in cand_indexes:
is_special = (word_ids[0] == self.tokenizer.cls_id) or (word_ids[0] == self.tokenizer.sep_id)
if is_special or (random.random() > self.mask_probability):
output_mask.extend([0] * len(word_ids))
masked_ids.extend(word_ids)
else:
output_mask.extend([1] * len(word_ids))
p = random.random()
# for 80%, replace with mask
if p < 0.8:
masked_ids.extend([mask_id] * len(word_ids))
# for 10%, replace by a random token
elif p < 0.9:
for _ in word_ids:
# randomly select a valid word
random_word = random.randrange(self.vocab_size)
while random_word in (self.tokenizer.cls_id, self.tokenizer.sep_id):
random_word = random.randrange(self.vocab_size)
masked_ids.append(random_word)
# for 10%, use same token
else:
masked_ids.extend(word_ids)
return masked_ids, output_mask
class BertPretrainingPreprocessedDataset(Dataset):
"""
Dataset for already preprocessed data.
"""
def __init__(self, input_file: str, max_predictions_per_seq: int):
"""
Args:
input_file: data file in hdf5 format with preprocessed data in array format
max_predictions_per_seq: maximum number of masked tokens per sequence. Need to be consistent with data in input file.
"""
self.input_file = input_file
self.max_predictions_per_seq = max_predictions_per_seq
f = load_h5(input_file)
keys = [
'input_ids',
'input_mask',
'segment_ids',
'masked_lm_positions',
'masked_lm_ids',
'next_sentence_labels',
]
self.inputs = [np.asarray(f[key][:]) for key in keys]
f.close()
def __len__(self):
'Denotes the total number of samples'
return len(self.inputs[0])
def __getitem__(self, index: int):
[input_ids, input_mask, segment_ids, masked_lm_positions, masked_lm_ids, next_sentence_labels] = [
input[index].astype(np.int64) for input in self.inputs
]
output_mask = np.zeros_like(input_ids)
output_ids = input_ids.copy()
index = self.max_predictions_per_seq
padded_mask_indices = (masked_lm_positions == 0).nonzero()
if len(padded_mask_indices[0]) != 0:
index = padded_mask_indices[0][0]
output_mask[masked_lm_positions[:index]] = 1.0
output_ids[masked_lm_positions[:index]] = masked_lm_ids[:index]
# input_mask = np.asarray(input_mask, dtype=np.float32)
# output_mask = np.asarray(output_mask, dtype=np.float32)
return (input_ids, segment_ids, input_mask, output_ids, output_mask, next_sentence_labels)
class BertPretrainingPreprocessedDataloader(DataLoader):
"""
Dataloader for already preprocessed data in hdf5 files that is already in the format expected by BERT model.
"""
def __init__(self, data_files: List[str], max_predictions_per_seq: int, batch_size: int, seed: Optional[int] = 42):
"""
Args:
data_files: list of data files in hdf5 format with preprocessed data in array format
max_predictions_per_seq: maximum number of masked tokens per sequence. Need to be consistent with data in input file.
batch_size: batch size per gpu per forward pass
seed: seed to ensure each gpu process opens the same data file in each iteration
"""
super().__init__(None, batch_size=batch_size)
self.random = random.Random(seed)
self.data_files = data_files
self.max_predictions_per_seq = max_predictions_per_seq
# def __len__(self):
# return sum([len(load_h5(data_file)['input_ids']) for data_file in self.data_files])//(self.batch_size)
def __iter__(self):
self.random.shuffle(self.data_files)
for data_file in self.data_files:
train_data = BertPretrainingPreprocessedDataset(
input_file=data_file, max_predictions_per_seq=self.max_predictions_per_seq
)
train_sampler = DistributedSampler(train_data)
# print("---")
# print(os.getpid(), train_sampler.rank, train_sampler.num_replicas, train_sampler.num_samples)
# print("---")
train_dataloader = DataLoader(
dataset=train_data, sampler=train_sampler, batch_size=self.batch_size, shuffle=False,
)
for x in train_dataloader:
yield x
@@ -0,0 +1,273 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pytorch Dataset with sentences packed into batches by length."""
import io
import json
import logging
import pickle
from collections import OrderedDict
from typing import Any
import braceexpand
import numpy as np
import webdataset as wd
from torch.utils.data import IterableDataset
from nemo.collections.nlp.data.data_utils.data_preprocessing import dataset_to_ids
from nemo.core import Dataset
__all__ = ['SentenceDataset', 'TarredSentenceDataset']
class SentenceDataset(Dataset):
def __init__(
self,
tokenizer: Any,
dataset: Any,
tokens_in_batch: int = 1024,
clean: bool = False,
cache_ids: bool = False,
max_seq_length: int = 512,
min_seq_length: int = 1,
):
self.tokenizer = tokenizer
self.tokens_in_batch = tokens_in_batch
ids = dataset_to_ids(dataset, tokenizer, cache_ids=cache_ids)
if clean:
ids = self.clean(ids, max_tokens=max_seq_length, min_tokens=min_seq_length)
self.batch_sent_ids, self.batch_elem_lengths = self.pack_data_into_batches(ids)
self.batches = self.pad_batches(ids)
def __len__(self):
return len(self.batches)
def __getitem__(self, idx):
ids = self.batches[idx]
mask = (ids != self.tokenizer.pad_id).astype(np.int32)
return ids, mask
def pad_batches(self, ids):
"""
Augments source and target ids in the batches with padding symbol
to make the lengths of all sentences in the batches equal.
"""
batches = []
for batch_elem_len, batch_sent_ids in zip(self.batch_elem_lengths, self.batch_sent_ids):
batch = self.tokenizer.pad_id * np.ones((len(batch_sent_ids), batch_elem_len), dtype=np.int)
for i, sentence_idx in enumerate(batch_sent_ids):
batch[i][: len(ids[sentence_idx])] = ids[sentence_idx]
batches.append(batch)
return batches
def pack_data_into_batches(self, ids):
"""
Takes two lists of source and target sentences, sorts them, and packs
into batches to minimize the use of padding tokens. Returns a list of
batches where each batch contains indices of sentences included into it
"""
# create buckets sorted by the number of src tokens
# each bucket is also sorted by the number of tgt tokens
buckets = {}
for i, line_ids in enumerate(ids):
len_ = len(line_ids)
if len_ not in buckets:
buckets[len_] = [i]
else:
buckets[len_].append(i)
for b_idx in buckets:
buckets[b_idx] = sorted(buckets[b_idx])
buckets = OrderedDict(sorted(buckets.items()))
batches = []
batch_elem_lengths = []
curr_batch = []
len_of_longest_sent = 0
for sent_len, bucket in buckets.items():
for sent_i in bucket:
if sent_len * (len(curr_batch) + 1) > self.tokens_in_batch:
if not curr_batch:
raise ValueError(
f"The limitation on number of tokens in batch {self.tokens_in_batch} is too strong."
f"Several sentences contain {sent_len} tokens."
)
batches.append(curr_batch)
batch_elem_lengths.append(sent_len)
curr_batch = []
curr_batch.append(sent_i)
len_of_longest_sent = sent_len
if curr_batch:
batches.append(curr_batch)
batch_elem_lengths.append(len_of_longest_sent)
return batches, batch_elem_lengths
def clean(self, ids, max_tokens=None, min_tokens=None):
"""
Cleans source and target sentences to get rid of noisy data.
Specifically, a pair of sentences is removed if
-- either source or target is longer than *max_tokens*
-- either source or target is shorter than *min_tokens*
-- absolute difference between source and target is larger than
*max_tokens_diff*
-- one sentence is *max_tokens_ratio* times longer than the other
"""
ids_ = []
for i in range(len(ids)):
len_ = len(ids[i])
if (max_tokens is not None and len_ > max_tokens) or (min_tokens is not None and len_ < min_tokens):
continue
ids_.append(ids[i])
return ids_
class TarredSentenceDataset(IterableDataset):
"""
A similar Dataset to the SentenceDataset, but which loads tarred tokenized pickle files.
Accepts a single JSON metadata file containing the total number of batches
as well as the path(s) to the tarball(s) containing the wav files.
Valid formats for the text_tar_filepaths argument include:
(1) a single string that can be brace-expanded, e.g. 'path/to/text.tar' or 'path/to/text_{1..100}.tar.gz', or
(2) a list of file paths that will not be brace-expanded, e.g. ['text_1.tar', 'text_2.tar', ...].
Note: For brace expansion in (1), there may be cases where `{x..y}` syntax cannot be used due to shell interference.
This occurs most commonly inside SLURM scripts. Therefore we provide a few equivalent replacements.
Supported opening braces - { <=> (, [, < and the special tag _OP_.
Supported closing braces - } <=> ), ], > and the special tag _CL_.
For SLURM based tasks, we suggest the use of the special tags for ease of use.
See the WebDataset documentation for more information about accepted data and input formats.
If using multiple processes the number of shards should be divisible by the number of workers to ensure an
even split among workers. If it is not divisible, logging will give a warning but training will proceed.
Additionally, please note that the len() of this DataLayer is assumed to be the number of tokens
of the text data. An incorrect manifest length may lead to some DataLoader issues down the line.
Args:
text_tar_filepaths: Either a list of tokenized text tarball filepaths, or a
string (can be brace-expandable).
metadata_path (str): Path to the metadata manifest.
encoder_tokenizer: Autokenizer wrapped BPE tokenizer model, such as YTTM
decoder_tokenizer: Autokenizer wrapped BPE tokenizer model, such as YTTM
shuffle_n (int): How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
Defaults to 0.
shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp.
- `scatter`: The default shard strategy applied by WebDataset, where each node gets
a unique set of shards, which are permanently pre-allocated and never changed at runtime.
- `replicate`: Optional shard strategy, where each node gets all of the set of shards
available in the tarred dataset, which are permanently pre-allocated and never changed at runtime.
The benefit of replication is that it allows each node to sample data points from the entire
dataset independently of other nodes, and reduces dependence on value of `shuffle_n`.
Note: Replicated strategy allows every node to sample the entire set of available tarfiles,
and therefore more than one node may sample the same tarfile, and even sample the same
data points! As such, there is no assured guarantee that all samples in the dataset will be
sampled at least once during 1 epoch.
global_rank (int): Worker rank, used for partitioning shards. Defaults to 0.
world_size (int): Total number of processes, used for partitioning shards. Defaults to 0.
reverse_lang_direction (bool): When True, swaps the source and target directions when returning minibatches.
"""
def __init__(
self,
text_tar_filepaths: str,
metadata_path: str,
tokenizer: str,
shuffle_n: int = 1,
shard_strategy: str = "scatter",
global_rank: int = 0,
world_size: int = 0,
):
super(TarredSentenceDataset, self).__init__()
self.tokenizer = tokenizer
self.pad_id = tokenizer.pad_id
valid_shard_strategies = ['scatter', 'replicate']
if shard_strategy not in valid_shard_strategies:
raise ValueError(f"`shard_strategy` must be one of {valid_shard_strategies}")
with open(metadata_path, 'r') as f:
metadata = json.load(f)
self.metadata = metadata
if isinstance(text_tar_filepaths, str):
# Replace '(', '[', '<' and '_OP_' with '{'
brace_keys_open = ['(', '[', '<', '_OP_']
for bkey in brace_keys_open:
if bkey in text_tar_filepaths:
text_tar_filepaths = text_tar_filepaths.replace(bkey, "{")
# Replace ')', ']', '>' and '_CL_' with '}'
brace_keys_close = [')', ']', '>', '_CL_']
for bkey in brace_keys_close:
if bkey in text_tar_filepaths:
text_tar_filepaths = text_tar_filepaths.replace(bkey, "}")
if isinstance(text_tar_filepaths, str):
# Brace expand
text_tar_filepaths = list(braceexpand.braceexpand(text_tar_filepaths))
if shard_strategy == 'scatter':
logging.info("All tarred dataset shards will be scattered evenly across all nodes.")
if len(text_tar_filepaths) % world_size != 0:
logging.warning(
f"Number of shards in tarred dataset ({len(text_tar_filepaths)}) is not divisible "
f"by number of distributed workers ({world_size})."
)
begin_idx = (len(text_tar_filepaths) // world_size) * global_rank
end_idx = begin_idx + (len(text_tar_filepaths) // world_size)
logging.info('Begin Index : %d' % (begin_idx))
logging.info('End Index : %d' % (end_idx))
text_tar_filepaths = text_tar_filepaths[begin_idx:end_idx]
logging.info(
"Partitioning tarred dataset: process (%d) taking shards [%d, %d)", global_rank, begin_idx, end_idx
)
elif shard_strategy == 'replicate':
logging.info("All tarred dataset shards will be replicated across all nodes.")
else:
raise ValueError(f"Invalid shard strategy ! Allowed values are : {valid_shard_strategies}")
self.tarpath = text_tar_filepaths
# Put together WebDataset
self._dataset = wd.WebDataset(urls=text_tar_filepaths, nodesplitter=None)
if shuffle_n > 0:
self._dataset = self._dataset.shuffle(shuffle_n)
else:
logging.info("WebDataset will not shuffle files within the tar files.")
self._dataset = self._dataset.rename(pkl='pkl', key='__key__').to_tuple('pkl', 'key').map(f=self._build_sample)
def _build_sample(self, fname):
# Load file
pkl_file, _ = fname
pkl_file = io.BytesIO(pkl_file)
data = pickle.load(pkl_file) # loads np.int64 vector
pkl_file.close()
ids = data["src"]
mask = (ids != self.pad_id).astype(np.int32)
return ids, mask
def __iter__(self):
return self._dataset.__iter__()
def __len__(self):
return self.metadata['num_batches']
@@ -0,0 +1,18 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.machine_translation.machine_translation_dataset import (
TarredTranslationDataset,
TranslationDataset,
)
@@ -0,0 +1,445 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pytorch Dataset for training Neural Machine Translation."""
import io
import json
import pickle
from collections import OrderedDict
from dataclasses import dataclass
from typing import Any, List, Optional
import braceexpand
import numpy as np
import webdataset as wd
from torch.utils.data import IterableDataset
from nemo.collections.nlp.data.data_utils.data_preprocessing import dataset_to_ids
from nemo.core import Dataset
from nemo.utils import logging
__all__ = ['TranslationDataset', 'TarredTranslationDataset']
@dataclass
class TranslationDataConfig:
src_file_name: Optional[Any] = None # Any = str or List[str]
tgt_file_name: Optional[Any] = None # Any = str or List[str]
use_tarred_dataset: bool = False
tar_files: Optional[Any] = None # Any = str or List[str]
metadata_file: Optional[Any] = None # Any = str or List[str]
lines_per_dataset_fragment: Optional[int] = 1000000
num_batches_per_tarfile: Optional[int] = 1000
shard_strategy: Optional[str] = 'scatter'
tokens_in_batch: int = 512
clean: bool = False
max_seq_length: int = 512
min_seq_length: int = 1
cache_ids: bool = False
cache_data_per_node: bool = False
use_cache: bool = False
shuffle: bool = False
num_samples: int = -1
drop_last: bool = False
pin_memory: bool = False
num_workers: int = 8
reverse_lang_direction: bool = False
load_from_tarred_dataset: bool = False
metadata_path: Optional[str] = None
tar_shuffle_n: int = 100
n_preproc_jobs: int = -2
tar_file_prefix: str = 'parallel'
concat_sampling_technique: Optional[str] = 'temperature'
concat_sampling_temperature: Optional[int] = 5
concat_sampling_probabilities: Optional[List[float]] = None
class TranslationDataset(Dataset):
def __init__(
self,
dataset_src: str,
dataset_tgt: str,
tokens_in_batch: int = 1024,
clean: bool = False,
max_seq_length: int = 512,
min_seq_length: int = 1,
max_seq_length_diff: int = 512,
max_seq_length_ratio: int = 512,
cache_ids: bool = False,
cache_data_per_node: bool = False,
use_cache: bool = False,
reverse_lang_direction: bool = False,
prepend_id: int = None,
):
self.dataset_src = dataset_src
self.dataset_tgt = dataset_tgt
self.tokens_in_batch = tokens_in_batch
self.cache_ids = cache_ids
self.use_cache = use_cache
self.clean = clean
self.cache_data_per_node = cache_data_per_node
self.max_seq_length = max_seq_length
self.min_seq_length = min_seq_length
self.max_seq_length_diff = max_seq_length_diff
self.max_seq_length_ratio = max_seq_length_ratio
self.reverse_lang_direction = reverse_lang_direction
self.prepend_id = prepend_id
# deprecation warnings for cache_ids, use_cache, and cache_data_per_node
if self.cache_ids is True or self.use_cache is True or self.cache_data_per_node is True:
logging.warning(
'Deprecation warning. self.cache_ids, self.use_cache, and self.cache_data_per_node will be removed. Data caching to be done with tarred datasets moving forward.'
)
def batchify(self, tokenizer_src, tokenizer_tgt):
src_ids = dataset_to_ids(
self.dataset_src,
tokenizer_src,
cache_ids=self.cache_ids,
cache_data_per_node=self.cache_data_per_node,
use_cache=self.use_cache,
)
tgt_ids = dataset_to_ids(
self.dataset_tgt,
tokenizer_tgt,
cache_ids=self.cache_ids,
cache_data_per_node=self.cache_data_per_node,
use_cache=self.use_cache,
)
if self.clean:
src_ids, tgt_ids = self.clean_src_and_target(
src_ids,
tgt_ids,
max_tokens=self.max_seq_length,
min_tokens=self.min_seq_length,
max_tokens_diff=self.max_seq_length_diff,
max_tokens_ratio=self.max_seq_length_ratio,
)
self.src_pad_id = tokenizer_src.pad_id
self.tgt_pad_id = tokenizer_tgt.pad_id
self.batch_indices = self.pack_data_into_batches(src_ids, tgt_ids)
self.batches = self.pad_batches(src_ids, tgt_ids, self.batch_indices)
def __len__(self):
return len(self.batches)
def __getitem__(self, idx):
src_ids = self.batches[idx]["src"]
tgt = self.batches[idx]["tgt"]
if self.reverse_lang_direction:
src_ids, tgt = tgt, src_ids
labels = tgt[:, 1:]
tgt_ids = tgt[:, :-1]
if self.prepend_id:
src_ids = np.insert(src_ids, 0, self.prepend_id, axis=-1)
src_mask = (src_ids != self.src_pad_id).astype(np.int32)
tgt_mask = (tgt_ids != self.tgt_pad_id).astype(np.int32)
return src_ids, src_mask, tgt_ids, tgt_mask, labels
def pad_batches(self, src_ids, tgt_ids, batch_indices):
"""
Augments source and target ids in the batches with padding symbol
to make the lengths of all sentences in the batches equal.
"""
batches = {}
for batch_idx, b in enumerate(batch_indices):
src_len = max([len(src_ids[i]) for i in b])
tgt_len = max([len(tgt_ids[i]) for i in b])
src_ids_ = self.src_pad_id * np.ones((len(b), src_len), dtype=np.int)
tgt_ids_ = self.tgt_pad_id * np.ones((len(b), tgt_len), dtype=np.int)
for i, sentence_idx in enumerate(b):
src_ids_[i][: len(src_ids[sentence_idx])] = src_ids[sentence_idx]
tgt_ids_[i][: len(tgt_ids[sentence_idx])] = tgt_ids[sentence_idx]
batches[batch_idx] = {"src": src_ids_, "tgt": tgt_ids_}
return batches
def pack_data_into_batches(self, src_ids, tgt_ids):
"""
Takes two lists of source and target sentences, sorts them, and packs
into batches to minimize the use of padding tokens. Returns a list of
batches where each batch contains indices of sentences included into it
"""
# create buckets sorted by the number of src tokens
# each bucket is also sorted by the number of tgt tokens
buckets = {}
for i, src_id in enumerate(src_ids):
src_len, tgt_len = len(src_id), len(tgt_ids[i])
if src_len not in buckets:
buckets[src_len] = [(tgt_len, i)]
else:
buckets[src_len].append((tgt_len, i))
for b_idx in buckets:
buckets[b_idx] = sorted(buckets[b_idx])
buckets = OrderedDict(sorted(buckets.items()))
indices = list(buckets.keys())
batches = [[]]
num_batches = 0
batch_size = 0
i = 0
src_len = 0
tgt_len = 0
while i < len(buckets):
while buckets[indices[i]]:
i_src = max(src_len, indices[i])
i_tgt = max(tgt_len, buckets[indices[i]][0][0])
try:
ip1_src = max(src_len, indices[i + 1])
ip1_tgt = max(tgt_len, buckets[indices[i + 1]][0][0])
except IndexError:
ip1_src = i_src + 1
ip1_tgt = i_tgt + 1
if i_src + i_tgt <= ip1_src + ip1_tgt:
src_len = i_src
tgt_len = i_tgt
_, idx = buckets[indices[i]].pop(0)
else:
src_len = ip1_src
tgt_len = ip1_tgt
_, idx = buckets[indices[i + 1]].pop(0)
batches[num_batches].append(idx)
batch_size += 1
if batch_size * (src_len + tgt_len) > self.tokens_in_batch:
num_examples_to_split = len(batches[num_batches])
batches_to_evict = 8 * ((num_examples_to_split - 1) // 8)
if batches_to_evict == 0:
batches_to_evict = num_examples_to_split
batches.append(batches[num_batches][batches_to_evict:])
batches[num_batches] = batches[num_batches][:batches_to_evict]
batch_size = num_examples_to_split - batches_to_evict
num_batches += 1
if batch_size > 0:
src_len = max([len(src_ids[j]) for j in batches[num_batches]])
tgt_len = max([len(tgt_ids[j]) for j in batches[num_batches]])
else:
src_len = 0
tgt_len = 0
break
if not buckets[indices[i]]:
i = i + 1
if not batches[-1]:
batches.pop(-1)
return batches
def clean_src_and_target(
self,
src_ids,
tgt_ids,
max_tokens=None,
min_tokens=None,
max_tokens_diff=None,
max_tokens_ratio=None,
filter_equal_src_and_dest=False,
):
"""
Cleans source and target sentences to get rid of noisy data.
Specifically, a pair of sentences is removed if
-- either source or target is longer than *max_tokens*
-- either source or target is shorter than *min_tokens*
-- absolute difference between source and target is larger than
*max_tokens_diff*
-- one sentence is *max_tokens_ratio* times longer than the other
"""
if len(src_ids) != len(tgt_ids):
raise ValueError("Source and target corpora have different lengths!")
src_ids_, tgt_ids_ = [], []
for i in range(len(src_ids)):
src_len, tgt_len = len(src_ids[i]), len(tgt_ids[i])
if (
(max_tokens is not None and (src_len > max_tokens or tgt_len > max_tokens))
or (min_tokens is not None and (src_len < min_tokens or tgt_len < min_tokens))
or (filter_equal_src_and_dest and src_ids[i] == tgt_ids[i])
or (max_tokens_diff is not None and np.abs(src_len - tgt_len) > max_tokens_diff)
):
continue
if max_tokens_ratio is not None:
ratio = max(src_len - 2, 1) / max(tgt_len - 2, 1)
if ratio > max_tokens_ratio or ratio < (1 / max_tokens_ratio):
continue
src_ids_.append(src_ids[i])
tgt_ids_.append(tgt_ids[i])
return src_ids_, tgt_ids_
class TarredTranslationDataset(IterableDataset):
"""
A similar Dataset to the TranslationDataset, but which loads tarred tokenized pickle files.
Accepts a single JSON metadata file containing the total number of batches
as well as the path(s) to the tarball(s) containing the pickled parallel dataset batch files.
Valid formats for the text_tar_filepaths argument include:
(1) a single string that can be brace-expanded, e.g. 'path/to/text.tar' or 'path/to/text_{1..100}.tar', or
(2) a list of file paths that will not be brace-expanded, e.g. ['text_1.tar', 'text_2.tar', ...].
Note: For brace expansion in (1), there may be cases where `{x..y}` syntax cannot be used due to shell interference.
This occurs most commonly inside SLURM scripts. Therefore we provide a few equivalent replacements.
Supported opening braces - { <=> (, [, < and the special tag _OP_.
Supported closing braces - } <=> ), ], > and the special tag _CL_.
For SLURM based tasks, we suggest the use of the special tags for ease of use.
See the WebDataset documentation for more information about accepted data and input formats.
If using multiple processes the number of shards should be divisible by the number of workers to ensure an
even split among workers. If it is not divisible, logging will give a warning but training will proceed.
Additionally, please note that the len() of this DataLayer is assumed to be the number of tokens
of the text data. An incorrect manifest length may lead to some DataLoader issues down the line.
Args:
text_tar_filepaths: Either a list of tokenized text tarball filepaths, or a
string (can be brace-expandable).
metadata_path (str): Path to the metadata manifest.
encoder_tokenizer: Autokenizer wrapped BPE tokenizer model, such as YTTM
decoder_tokenizer: Autokenizer wrapped BPE tokenizer model, such as YTTM
shuffle_n (int): How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
Defaults to 0.
shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp.
- `scatter`: The default shard strategy applied by WebDataset, where each node gets
a unique set of shards, which are permanently pre-allocated and never changed at runtime.
- `replicate`: Optional shard strategy, where each node gets all of the set of shards
available in the tarred dataset, which are permanently pre-allocated and never changed at runtime.
The benefit of replication is that it allows each node to sample data points from the entire
dataset independently of other nodes, and reduces dependence on value of `shuffle_n`.
Note: Replicated strategy allows every node to sample the entire set of available tarfiles,
and therefore more than one node may sample the same tarfile, and even sample the same
data points! As such, there is no assured guarantee that all samples in the dataset will be
sampled at least once during 1 epoch.
global_rank (int): Worker rank, used for partitioning shards. Defaults to 0.
world_size (int): Total number of processes, used for partitioning shards. Defaults to 1.
reverse_lang_direction (bool): When True, swaps the source and target directions when returning minibatches.
prepend_id (int): Prepends the specificed token id to the start of every source sentence. Defaults to None.
"""
def __init__(
self,
text_tar_filepaths: str,
metadata_path: str,
encoder_tokenizer: str,
decoder_tokenizer: str,
shuffle_n: int = 1,
shard_strategy: str = "scatter",
global_rank: int = 0,
world_size: int = 1,
reverse_lang_direction: bool = False,
prepend_id: int = None,
):
super(TarredTranslationDataset, self).__init__()
self.encoder_tokenizer = encoder_tokenizer
self.decoder_tokenizer = decoder_tokenizer
self.reverse_lang_direction = reverse_lang_direction
self.src_pad_id = encoder_tokenizer.pad_id
self.tgt_pad_id = decoder_tokenizer.pad_id
self.prepend_id = prepend_id
valid_shard_strategies = ['scatter', 'replicate']
if shard_strategy not in valid_shard_strategies:
raise ValueError(f"`shard_strategy` must be one of {valid_shard_strategies}")
with open(metadata_path, 'r') as f:
metadata = json.load(f)
self.metadata = metadata
if isinstance(text_tar_filepaths, str):
# Replace '(', '[', '<' and '_OP_' with '{'
brace_keys_open = ['(', '[', '<', '_OP_']
for bkey in brace_keys_open:
if bkey in text_tar_filepaths:
text_tar_filepaths = text_tar_filepaths.replace(bkey, "{")
# Replace ')', ']', '>' and '_CL_' with '}'
brace_keys_close = [')', ']', '>', '_CL_']
for bkey in brace_keys_close:
if bkey in text_tar_filepaths:
text_tar_filepaths = text_tar_filepaths.replace(bkey, "}")
if isinstance(text_tar_filepaths, str):
# Brace expand
text_tar_filepaths = list(braceexpand.braceexpand(text_tar_filepaths))
if shard_strategy == 'scatter':
logging.info("All tarred dataset shards will be scattered evenly across all nodes.")
if len(text_tar_filepaths) % world_size != 0:
logging.warning(
f"Number of shards in tarred dataset ({len(text_tar_filepaths)}) is not divisible "
f"by number of distributed workers ({world_size})."
)
begin_idx = (len(text_tar_filepaths) // world_size) * global_rank
end_idx = begin_idx + (len(text_tar_filepaths) // world_size)
logging.info('Begin Index : %d' % (begin_idx))
logging.info('End Index : %d' % (end_idx))
text_tar_filepaths = text_tar_filepaths[begin_idx:end_idx]
logging.info(
"Partitioning tarred dataset: process (%d) taking shards [%d, %d)", global_rank, begin_idx, end_idx
)
self.length = self.metadata['num_batches'] // world_size
elif shard_strategy == 'replicate':
logging.info("All tarred dataset shards will be replicated across all nodes.")
self.length = self.metadata['num_batches']
else:
raise ValueError(f"Invalid shard strategy ! Allowed values are : {valid_shard_strategies}")
self.tarpath = text_tar_filepaths
# Put together WebDataset
self._dataset = wd.WebDataset(urls=text_tar_filepaths, nodesplitter=None)
if shuffle_n > 0:
self._dataset = self._dataset.shuffle(shuffle_n)
else:
logging.info("WebDataset will not shuffle files within the tar files.")
self._dataset = self._dataset.rename(pkl='pkl', key='__key__').to_tuple('pkl', 'key').map(f=self._build_sample)
def _build_sample(self, fname):
# Load file
pkl_file, _ = fname
pkl_file = io.BytesIO(pkl_file)
data = pickle.load(pkl_file) # loads np.int64 vector
pkl_file.close()
src_ids = data["src"]
tgt = data["tgt"]
if self.reverse_lang_direction:
src_ids, tgt = tgt, src_ids
labels = tgt[:, 1:]
tgt_ids = tgt[:, :-1]
if self.prepend_id:
src_ids = np.insert(src_ids, 0, self.prepend_id, axis=-1)
src_mask = (src_ids != self.src_pad_id).astype(np.int32)
tgt_mask = (tgt_ids != self.tgt_pad_id).astype(np.int32)
return src_ids, src_mask, tgt_ids, tgt_mask, labels
def __iter__(self):
return self._dataset.__iter__()
def __len__(self):
return self.length
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.neural_machine_translation.neural_machine_translation_dataset import (
NeuralMachineTranslationDataset,
)
@@ -0,0 +1,146 @@
# Copyright (c) 2020, MeetKai Inc. All rights reserved.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Dict, List, Optional, Tuple
import numpy as np
from nemo.collections.common.tokenizers import AutoTokenizer
from nemo.core.classes import Dataset
from nemo.core.neural_types import ChannelType, MaskType, NeuralType
__all__ = ["NeuralMachineTranslationDataset"]
class NeuralMachineTranslationDataset(Dataset):
"""A dataset class that converts raw data to a dataset that can be used by NeuralMachineTranslationModel.
Args:
filepath: .tsv file to sequence + label.
the first line is header (sentence [tab] label)
each line should be [sentence][tab][label]
encoder_tokenizer: encoder tokenizer object such as AutoTokenizer
decoder_tokenizer: decoder tokenizer object. If using BART or end to end model, set this to encoder_tokenizer
max_seq_length: max sequence length including bos and eos tokens
num_samples: number of samples you want to use for the dataset. If -1, use all dataset. Useful for testing.
convert_labels: if true, converts labels for masked lm and updates pad_id to -100
for hf masked loss
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
return {
"input_ids": NeuralType(("B", "T"), ChannelType()),
"attention_mask": NeuralType(("B", "T"), MaskType()),
"decoder_input_ids": NeuralType(("B", "T"), ChannelType()),
"lm_labels": NeuralType(("B", "T"), ChannelType()),
}
def __init__(
self,
filepath: str,
encoder_tokenizer: AutoTokenizer,
decoder_tokenizer: AutoTokenizer,
encoder_add_special_tokens: bool,
decoder_add_special_tokens: bool,
max_seq_length: int,
num_samples: int = -1,
convert_labels: bool = False,
):
self.filepath = filepath
self.encoder_tokenizer = encoder_tokenizer
self.decoder_tokenizer = decoder_tokenizer
self.encoder_add_special_tokens = encoder_add_special_tokens
self.decoder_add_special_tokens = decoder_add_special_tokens
self.max_seq_length = max_seq_length
self.num_samples = num_samples
self.convert_labels = convert_labels
if num_samples == 0:
raise ValueError("num_samples has to be positive.", num_samples)
if self.max_seq_length and self.max_seq_length <= 2:
self.max_seq_length = None
if not os.path.exists(filepath):
raise FileNotFoundError(
f"{filepath} not found. The filepath must be set in train_ds.filepath and validation_ds.filepath."
)
with open(filepath) as f:
lines = f.readlines()[1:]
if num_samples > 0:
lines = lines[:num_samples]
input_ids, input_masks, label_ids = [], [], []
for line in lines:
try:
sentence, label = line.split("\t")
except ValueError:
raise ValueError("Each line of input file should contain the format [sentence][tab][label].")
ids, mask = self.text_to_ids(
sentence, tokenizer=encoder_tokenizer, add_special_tokens=encoder_add_special_tokens
)
input_ids.append(ids)
input_masks.append(mask)
label_ids.append(
self.text_to_ids(label, tokenizer=decoder_tokenizer, add_special_tokens=decoder_add_special_tokens)[0]
)
self.input_ids = np.asarray(input_ids)
self.input_masks = np.asarray(input_masks)
self.label_ids = np.asarray(label_ids)
def text_to_ids(
self, text: str, tokenizer: AutoTokenizer, add_special_tokens=False
) -> Tuple[List[int], List[int]]:
"""Converts text to ids. Truncates and adds padding."""
text_tokens = tokenizer.text_to_ids(text.strip())
num_special_tokens = 2 if add_special_tokens else 0
if self.max_seq_length and self.max_seq_length > num_special_tokens:
text_tokens = text_tokens[: self.max_seq_length - num_special_tokens]
if add_special_tokens:
text_tokens = [tokenizer.bos_id] + text_tokens + [tokenizer.eos_id]
mask = [1] * len(text_tokens)
if self.max_seq_length and self.max_seq_length > num_special_tokens:
pad_length = self.max_seq_length - len(text_tokens)
text_tokens += [tokenizer.pad_id] * pad_length
mask += [0] * pad_length
return text_tokens, mask
def __len__(self):
return len(self.input_ids)
def convert_label_ids(self, label_ids: List[int]) -> Tuple[List[int], List[int]]:
decoder_input_ids = label_ids[:-1]
lm_labels = label_ids[1:].copy()
lm_labels[label_ids[1:] == self.decoder_tokenizer.pad_id] = -100 # for huggingface masked lm loss
return decoder_input_ids, lm_labels
def __getitem__(self, idx):
if self.convert_labels:
decoder_input_ids, lm_labels = self.convert_label_ids(self.label_ids[idx])
else:
decoder_input_ids = self.label_ids[idx]
lm_labels = self.label_ids[idx]
return self.input_ids[idx], self.input_masks[idx], decoder_input_ids, lm_labels
@@ -0,0 +1,13 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,443 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import json
import os
import pickle
from typing import Dict, List, Optional
import numpy as np
import torch
from nemo.collections.common.parts.utils import _compute_softmax
from nemo.collections.nlp.data.question_answering_squad.qa_squad_processing import (
EVALUATION_MODE,
INFERENCE_MODE,
TRAINING_MODE,
SquadProcessor,
apply_no_ans_threshold,
convert_examples_to_features,
exact_match_score,
f1_score,
find_all_best_thresh,
get_best_indexes,
get_final_text,
make_eval_dict,
merge_eval,
normalize_answer,
)
from nemo.core.classes import Dataset
from nemo.utils import logging
__all__ = ['SquadDataset']
class SquadDataset(Dataset):
"""
Creates SQuAD dataset for Question Answering.
Args:
data_file (str): train.*.json eval.*.json or test.*.json.
tokenizer (obj): Tokenizer object, e.g. AutoTokenizer.
version_2_with_negative (bool): True if training should allow
unanswerable questions.
doc_stride (int): When splitting up a long document into chunks,
how much stride to take between chunks.
max_query_length (iny): All training files which have a duration less
than min_duration are dropped. Can't be used if the `utt2dur` file
does not exist. Defaults to None.
max_seq_length (int): All training files which have a duration more
than max_duration are dropped. Can't be used if the `utt2dur` file
does not exist. Defaults to None.
num_samples: number of samples you want to use for the dataset.
If -1, use all dataset. Useful for testing.
mode (str): Use TRAINING_MODE/EVALUATION_MODE/INFERENCE_MODE to define between
training, evaluation and inference dataset.
use_cache (bool): Caches preprocessed data for future usage
"""
def __init__(
self,
data_file: str,
tokenizer: object,
doc_stride: int,
max_query_length: int,
max_seq_length: int,
version_2_with_negative: bool,
num_samples: int,
mode: str,
use_cache: bool,
):
self.tokenizer = tokenizer
self.version_2_with_negative = version_2_with_negative
self.processor = SquadProcessor(data_file=data_file, mode=mode)
self.mode = mode
if mode not in [TRAINING_MODE, EVALUATION_MODE, INFERENCE_MODE]:
raise ValueError(
f"mode should be either {TRAINING_MODE}, {EVALUATION_MODE}, {INFERENCE_MODE} but got {mode}"
)
self.examples = self.processor.get_examples()
vocab_size = getattr(tokenizer, "vocab_size", 0)
cached_features_file = (
data_file
+ '_cache'
+ '_{}_{}_{}_{}_{}_{}_{}'.format(
mode,
tokenizer.name,
str(vocab_size),
str(max_seq_length),
str(doc_stride),
str(max_query_length),
str(num_samples),
)
)
# check number of samples. Should be either -1 not to limit or positive number
if num_samples == 0:
raise ValueError(
f"num_samples has to be positive or -1 (to use the entire dataset), however got {num_samples}."
)
elif num_samples > 0:
self.examples = self.examples[:num_samples]
if use_cache and os.path.exists(cached_features_file):
logging.info(f"loading from {cached_features_file}")
with open(cached_features_file, "rb") as reader:
self.features = pickle.load(reader)
else:
logging.info(f"Preprocessing data.")
self.features = convert_examples_to_features(
examples=self.examples,
tokenizer=tokenizer,
max_seq_length=max_seq_length,
doc_stride=doc_stride,
max_query_length=max_query_length,
has_groundtruth=mode != INFERENCE_MODE,
)
if use_cache:
master_device = not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0
if master_device:
logging.info(" Saving train features into cached file %s", cached_features_file)
with open(cached_features_file, "wb") as writer:
pickle.dump(self.features, writer)
def __len__(self):
return len(self.features)
def __getitem__(self, idx):
feature = self.features[idx]
if self.mode == INFERENCE_MODE:
return (
np.array(feature.input_ids),
np.array(feature.segment_ids),
np.array(feature.input_mask),
np.array(feature.unique_id),
)
else:
return (
np.array(feature.input_ids),
np.array(feature.segment_ids),
np.array(feature.input_mask),
np.array(feature.unique_id),
np.array(feature.start_position),
np.array(feature.end_position),
)
def get_predictions(
self,
unique_ids: List[int],
start_logits: List[List[float]],
end_logits: List[List[float]],
n_best_size: int,
max_answer_length: int,
do_lower_case: bool,
version_2_with_negative: bool,
null_score_diff_threshold: float,
):
example_index_to_features = collections.defaultdict(list)
unique_id_to_pos = {}
for index, unique_id in enumerate(unique_ids):
unique_id_to_pos[unique_id] = index
for feature in self.features:
example_index_to_features[feature.example_index].append(feature)
_PrelimPrediction = collections.namedtuple(
"PrelimPrediction", ["feature_index", "start_index", "end_index", "start_logit", "end_logit"]
)
all_predictions = collections.OrderedDict()
all_nbest_json = collections.OrderedDict()
scores_diff_json = collections.OrderedDict()
for (example_index, example) in enumerate(self.examples):
# finish this loop if we went through all batch examples
if example_index >= len(unique_ids):
break
features = example_index_to_features[example_index]
prelim_predictions = []
# keep track of the minimum score of null start+end of position 0
# large and positive
score_null = 1000000
# the paragraph slice with min null score
min_null_feature_index = 0
# start logit at the slice with min null score
null_start_logit = 0
# end logit at the slice with min null score
null_end_logit = 0
for (feature_index, feature) in enumerate(features):
pos = unique_id_to_pos[feature.unique_id]
start_indexes = get_best_indexes(start_logits[pos], n_best_size)
end_indexes = get_best_indexes(end_logits[pos], n_best_size)
# if we could have irrelevant answers,
# get the min score of irrelevant
if version_2_with_negative:
feature_null_score = start_logits[pos][0] + end_logits[pos][0]
if feature_null_score < score_null:
score_null = feature_null_score
min_null_feature_index = feature_index
null_start_logit = start_logits[pos][0]
null_end_logit = end_logits[pos][0]
for start_index in start_indexes:
for end_index in end_indexes:
# We could hypothetically create invalid predictions,
# e.g., predict that the start of the span is in the
# question. We throw out all invalid predictions.
if start_index >= len(feature.tokens):
continue
if end_index >= len(feature.tokens):
continue
if start_index not in feature.token_to_orig_map:
continue
if end_index not in feature.token_to_orig_map:
continue
if not feature.token_is_max_context.get(start_index, False):
continue
if end_index < start_index:
continue
length = end_index - start_index + 1
if length > max_answer_length:
continue
prelim_predictions.append(
_PrelimPrediction(
feature_index=feature_index,
start_index=start_index,
end_index=end_index,
start_logit=start_logits[pos][start_index],
end_logit=end_logits[pos][end_index],
)
)
if version_2_with_negative:
prelim_predictions.append(
_PrelimPrediction(
feature_index=min_null_feature_index,
start_index=0,
end_index=0,
start_logit=null_start_logit,
end_logit=null_end_logit,
)
)
prelim_predictions = sorted(prelim_predictions, key=lambda x: (x.start_logit + x.end_logit), reverse=True)
_NbestPrediction = collections.namedtuple("NbestPrediction", ["text", "start_logit", "end_logit"])
seen_predictions = {}
nbest = []
for pred in prelim_predictions:
if len(nbest) >= n_best_size:
break
feature = features[pred.feature_index]
if pred.start_index > 0: # this is a non-null prediction
tok_tokens = feature.tokens[pred.start_index : (pred.end_index + 1)]
orig_doc_start = feature.token_to_orig_map[pred.start_index]
orig_doc_end = feature.token_to_orig_map[pred.end_index]
orig_tokens = example.doc_tokens[orig_doc_start : (orig_doc_end + 1)]
tok_text = " ".join(tok_tokens)
# De-tokenize WordPieces that have been split off.
tok_text = tok_text.replace(" ##", "")
tok_text = tok_text.replace("##", "")
# Clean whitespace
tok_text = tok_text.strip()
tok_text = " ".join(tok_text.split())
orig_text = " ".join(orig_tokens)
final_text = get_final_text(tok_text, orig_text, do_lower_case)
if final_text in seen_predictions:
continue
seen_predictions[final_text] = True
else:
final_text = ""
seen_predictions[final_text] = True
nbest.append(_NbestPrediction(text=final_text, start_logit=pred.start_logit, end_logit=pred.end_logit))
# if we didn't include the empty option in the n-best, include it
if version_2_with_negative:
if "" not in seen_predictions:
nbest.append(_NbestPrediction(text="", start_logit=null_start_logit, end_logit=null_end_logit))
# In very rare edge cases we could only
# have single null pred. We just create a nonce prediction
# in this case to avoid failure.
if len(nbest) == 1:
nbest.insert(0, _NbestPrediction(text="empty", start_logit=0.0, end_logit=0.0))
# In very rare edge cases we could have no valid predictions. So we
# just create a nonce prediction in this case to avoid failure.
if not nbest:
nbest.append(_NbestPrediction(text="empty", start_logit=0.0, end_logit=0.0))
assert len(nbest) >= 1
total_scores = []
best_non_null_entry = None
for entry in nbest:
total_scores.append(entry.start_logit + entry.end_logit)
if not best_non_null_entry:
if entry.text:
best_non_null_entry = entry
probs = _compute_softmax(total_scores)
nbest_json = []
for (i, entry) in enumerate(nbest):
output = collections.OrderedDict()
output["question"] = example.question_text
output["text"] = entry.text
output["probability"] = probs[i]
output["start_logit"] = (
entry.start_logit if isinstance(entry.start_logit, float) else list(entry.start_logit)
)
output["end_logit"] = entry.end_logit if isinstance(entry.end_logit, float) else list(entry.end_logit)
nbest_json.append(output)
assert len(nbest_json) >= 1
if not version_2_with_negative:
all_predictions[example.qas_id] = nbest_json[0]["text"]
else:
# predict "" iff the null score -
# the score of best non-null > threshold
score_diff = score_null - best_non_null_entry.start_logit - (best_non_null_entry.end_logit)
scores_diff_json[example.qas_id] = score_diff
if score_diff > null_score_diff_threshold:
all_predictions[example.qas_id] = ""
else:
all_predictions[example.qas_id] = best_non_null_entry.text
all_nbest_json[example.qas_id] = nbest_json
return all_predictions, all_nbest_json, scores_diff_json
def evaluate_predictions(
self,
all_predictions: Dict[str, str],
no_answer_probs: Optional[float] = None,
no_answer_probability_threshold: float = 1.0,
):
qas_id_to_has_answer = {
example.qas_id: bool(example.answers) for example in self.examples[: len(all_predictions)]
}
has_answer_qids = [qas_id for qas_id, has_answer in qas_id_to_has_answer.items() if has_answer]
no_answer_qids = [qas_id for qas_id, has_answer in qas_id_to_has_answer.items() if not has_answer]
if no_answer_probs is None:
no_answer_probs = {k: 0.0 for k in all_predictions}
exact, f1 = self.get_raw_scores(all_predictions)
exact_threshold = apply_no_ans_threshold(
exact, no_answer_probs, qas_id_to_has_answer, no_answer_probability_threshold
)
f1_threshold = apply_no_ans_threshold(
f1, no_answer_probs, qas_id_to_has_answer, no_answer_probability_threshold
)
evaluation = make_eval_dict(exact_threshold, f1_threshold)
if has_answer_qids:
has_ans_eval = make_eval_dict(exact_threshold, f1_threshold, qid_list=has_answer_qids)
merge_eval(evaluation, has_ans_eval, "HasAns")
if no_answer_qids:
no_ans_eval = make_eval_dict(exact_threshold, f1_threshold, qid_list=no_answer_qids)
merge_eval(evaluation, no_ans_eval, "NoAns")
if no_answer_probs:
find_all_best_thresh(evaluation, all_predictions, exact, f1, no_answer_probs, qas_id_to_has_answer)
return evaluation["best_exact"], evaluation["best_f1"]
def get_raw_scores(self, preds: Dict[str, str]):
"""
Computes the exact and f1 scores from the examples
and the model predictions
"""
exact_scores = {}
f1_scores = {}
for example in self.examples:
qas_id = example.qas_id
gold_answers = [answer["text"] for answer in example.answers if normalize_answer(answer["text"])]
if not gold_answers:
# For unanswerable questions,
# only correct answer is empty string
gold_answers = [""]
if qas_id not in preds:
logging.warning("Missing prediction for %s" % qas_id)
continue
prediction = preds[qas_id]
exact_scores[qas_id] = max(exact_match_score(a, prediction) for a in gold_answers)
f1_scores[qas_id] = max(f1_score(a, prediction) for a in gold_answers)
return exact_scores, f1_scores
def evaluate(
self,
unique_ids: List[str],
start_logits: List[List[float]],
end_logits: List[List[float]],
n_best_size: int,
max_answer_length: int,
do_lower_case: bool,
version_2_with_negative: bool,
null_score_diff_threshold: float,
):
(all_predictions, all_nbest_json, scores_diff_json) = self.get_predictions(
unique_ids,
start_logits,
end_logits,
n_best_size,
max_answer_length,
do_lower_case,
version_2_with_negative,
null_score_diff_threshold,
)
exact_match, f1 = self.evaluate_predictions(all_predictions)
return exact_match, f1, all_predictions, all_nbest_json
@@ -0,0 +1,644 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import json
from typing import Dict, List, Optional
from tqdm import tqdm
from transformers.models.bert.tokenization_bert import BasicTokenizer
from nemo.collections.nlp.data.data_utils import DataProcessor, is_whitespace, normalize_answer
from nemo.utils import logging
"""
Utility functions for Question Answering NLP tasks
Some parts of this code were adapted from the HuggingFace library at
https://github.com/huggingface/transformers
"""
TRAINING_MODE = "train"
EVALUATION_MODE = "eval"
INFERENCE_MODE = "infer"
def _get_tokens(s):
"""get normalized tokens"""
if not s:
return []
return normalize_answer(s).split()
def get_best_indexes(logits, n_best_size):
"""Get the n-best logits from a list."""
index_and_score = sorted(enumerate(logits), key=lambda x: x[1], reverse=True)
best_indexes = []
for i in range(len(index_and_score)):
if i >= n_best_size:
break
best_indexes.append(index_and_score[i][0])
return best_indexes
def get_final_text(pred_text: str, orig_text: str, do_lower_case: bool, verbose_logging: bool = False):
"""Project the tokenized prediction back to the original text.
When we created the data, we kept track of the alignment between original
(whitespace tokenized) tokens and our WordPiece tokenized tokens. So
now `orig_text` contains the span of our original text corresponding to
the span that we predicted.
However, `orig_text` may contain extra characters that we don't want in
our prediction.
For example, let's say:
pred_text = steve smith
orig_text = Steve Smith's
We don't want to return `orig_text` because it contains the extra "'s".
We don't want to return `pred_text` because it's already been normalized
(the SQuAD eval script also does punctuation stripping/lower casing but
our tokenizer does additional normalization like stripping accent
characters).
What we really want to return is "Steve Smith".
Therefore, we have to apply a semi-complicated alignment heuristic
between `pred_text` and `orig_text` to get a character-to-character
alignment. This can fail in certain cases in which case we just return
`orig_text`."""
def _strip_spaces(text):
ns_chars = []
ns_to_s_map = collections.OrderedDict()
for (i, c) in enumerate(text):
if c == " ":
continue
ns_to_s_map[len(ns_chars)] = i
ns_chars.append(c)
ns_text = "".join(ns_chars)
return ns_text, ns_to_s_map
# We first tokenize `orig_text`, strip whitespace from the result
# and `pred_text`, and check if they are the same length. If they are
# NOT the same length, the heuristic has failed. If they are the same
# length, we assume the characters are one-to-one aligned.
tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
tok_text = " ".join(tokenizer.tokenize(orig_text))
start_position = tok_text.find(pred_text)
if start_position == -1:
if verbose_logging:
logging.warning("Unable to find text: '%s' in '%s'" % (pred_text, orig_text))
return orig_text
end_position = start_position + len(pred_text) - 1
(orig_ns_text, orig_ns_to_s_map) = _strip_spaces(orig_text)
(tok_ns_text, tok_ns_to_s_map) = _strip_spaces(tok_text)
if len(orig_ns_text) != len(tok_ns_text):
if verbose_logging:
logging.warning(
"Length not equal after stripping spaces: '%s' vs '%s'", orig_ns_text, tok_ns_text,
)
return orig_text
# We then project the characters in `pred_text` back to `orig_text` using
# the character-to-character alignment.
tok_s_to_ns_map = {}
for (i, tok_index) in tok_ns_to_s_map.items():
tok_s_to_ns_map[tok_index] = i
orig_start_position = None
if start_position in tok_s_to_ns_map:
ns_start_position = tok_s_to_ns_map[start_position]
if ns_start_position in orig_ns_to_s_map:
orig_start_position = orig_ns_to_s_map[ns_start_position]
if orig_start_position is None:
if verbose_logging:
logging.warning("Couldn't map start position")
return orig_text
orig_end_position = None
if end_position in tok_s_to_ns_map:
ns_end_position = tok_s_to_ns_map[end_position]
if ns_end_position in orig_ns_to_s_map:
orig_end_position = orig_ns_to_s_map[ns_end_position]
if orig_end_position is None:
if verbose_logging:
logging.warning("Couldn't map end position")
return orig_text
output_text = orig_text[orig_start_position : (orig_end_position + 1)]
return output_text
def f1_score(prediction, ground_truth):
"""computes f1 score between prediction and ground truth"""
prediction_tokens = _get_tokens(prediction)
ground_truth_tokens = _get_tokens(ground_truth)
common = collections.Counter(prediction_tokens) & collections.Counter(ground_truth_tokens)
num_same = sum(common.values())
if len(ground_truth_tokens) == 0 or len(prediction_tokens) == 0:
# If either is no-answer, then F1 is 1 if they agree, 0 otherwise
return int(ground_truth_tokens == prediction_tokens)
if num_same == 0:
return 0
precision = 1.0 * num_same / len(prediction_tokens)
recall = 1.0 * num_same / len(ground_truth_tokens)
f1 = (2 * precision * recall) / (precision + recall)
return f1
def exact_match_score(prediction, ground_truth):
"""computes exact match between prediction and ground truth"""
return int(normalize_answer(prediction) == normalize_answer(ground_truth))
def apply_no_ans_threshold(scores, na_probs, qid_to_has_ans, na_prob_thresh):
"""Applies no answer threshhold"""
new_scores = {}
for qid, s in scores.items():
pred_na = na_probs[qid] > na_prob_thresh
if pred_na:
new_scores[qid] = float(not qid_to_has_ans[qid])
else:
new_scores[qid] = s
return new_scores
def make_eval_dict(exact_scores, f1_scores, qid_list=None):
"""returns dictionary with formatted evaluation scores"""
if not qid_list:
total = len(exact_scores)
return collections.OrderedDict(
[
("exact", 100.0 * sum(exact_scores.values()) / total),
("f1", 100.0 * sum(f1_scores.values()) / total),
("total", total),
]
)
else:
total = len(qid_list)
return collections.OrderedDict(
[
("exact", 100.0 * sum(exact_scores[k] for k in qid_list) / total),
("f1", 100.0 * sum(f1_scores[k] for k in qid_list) / total),
("total", total),
]
)
def merge_eval(main_eval, new_eval, prefix):
"""merges 2 evaluation dictionaries into the first one by adding prefix as key for name collision handling"""
for k in new_eval:
main_eval["%s_%s" % (prefix, k)] = new_eval[k]
def find_all_best_thresh(main_eval, preds, exact_raw, f1_raw, na_probs, qid_to_has_ans):
"""
Find best threshholds to maximize all evaluation metrics.
"""
best_exact, exact_thresh = _find_best_thresh(preds, exact_raw, na_probs, qid_to_has_ans)
best_f1, f1_thresh = _find_best_thresh(preds, f1_raw, na_probs, qid_to_has_ans)
main_eval["best_exact"] = best_exact
main_eval["best_exact_thresh"] = exact_thresh
main_eval["best_f1"] = best_f1
main_eval["best_f1_thresh"] = f1_thresh
def _find_best_thresh(preds, scores, na_probs, qid_to_has_ans):
"""
Find best threshhold to maximize evaluation metric
"""
num_no_ans = sum(1 for k in qid_to_has_ans if not qid_to_has_ans[k])
cur_score = num_no_ans
best_score = cur_score
best_thresh = 0.0
qid_list = sorted(na_probs, key=lambda k: na_probs[k])
for _, qid in enumerate(qid_list):
if qid not in scores:
continue
if qid_to_has_ans[qid]:
diff = scores[qid]
else:
if preds[qid]:
diff = -1
else:
diff = 0
cur_score += diff
if cur_score > best_score:
best_score = cur_score
best_thresh = na_probs[qid]
return 100.0 * best_score / len(scores), best_thresh
class SquadProcessor(DataProcessor):
"""
Processor for the SQuAD data set.
used by the version 1.1 and version 2.0 of SQuAD, respectively.
Args:
data_file: data file path
mode: TRAINING_MODE/EVALUATION_MODE/INFERENCE_MODE for creating training/evaluation/inference dataset
"""
def __init__(self, data_file: str, mode: str):
self.data_file = data_file
self.mode = mode
def get_examples(self):
if self.data_file is None:
raise ValueError(f"{self.mode} data file is None.")
with open(self.data_file, "r", encoding="utf-8") as reader:
input_data = json.load(reader)["data"]
return self._create_examples(input_data, set_type=self.mode)
def _create_examples(self, input_data, set_type):
examples = []
for entry in tqdm(input_data):
title = entry["title"]
for paragraph in entry["paragraphs"]:
context_text = paragraph["context"]
for qa in paragraph["qas"]:
qas_id = qa["id"]
question_text = qa["question"]
start_position_character = None
answer_text = None
answers = []
if "is_impossible" in qa:
is_impossible = qa["is_impossible"]
else:
is_impossible = False
if not is_impossible:
if set_type in [TRAINING_MODE, EVALUATION_MODE]:
answer = qa["answers"][0]
answer_text = answer["text"]
start_position_character = answer["answer_start"]
if set_type == EVALUATION_MODE:
answers = qa["answers"]
example = SquadExample(
qas_id=qas_id,
question_text=question_text,
context_text=context_text,
answer_text=answer_text,
start_position_character=start_position_character,
title=title,
is_impossible=is_impossible,
answers=answers,
)
examples.append(example)
return examples
class SquadExample(object):
"""
A single training/test example for the Squad dataset, as loaded from disk.
Args:
qas_id: The example's unique identifier
question_text: The question string
context_text: The context string
answer_text: The answer string
start_position_character: The character position of the start of
the answer, 0 indexed
title: The title of the example
answers: None by default, this is used during evaluation.
Holds answers as well as their start positions.
is_impossible: False by default, set to True if the example has
no possible answer.
"""
def __init__(
self,
qas_id: str,
question_text: str,
context_text: str,
answer_text: str,
start_position_character: int,
title: str,
answers: List[str] = [],
is_impossible: bool = False,
):
self.qas_id = qas_id
self.question_text = question_text
self.context_text = context_text
self.answer_text = answer_text
self.title = title
self.is_impossible = is_impossible
self.answers = answers
self.start_position, self.end_position = 0, 0
doc_tokens = []
char_to_word_offset = []
prev_is_whitespace = True
# Split on whitespace so that different tokens
# may be attributed to their original position.
# ex: context_text = ["hi yo"]
# char_to_word_offset = [0, 0, 0, 1, 1]
# doc_tokens = ["hi", "yo"]
for c in self.context_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)
self.doc_tokens = doc_tokens
self.char_to_word_offset = char_to_word_offset
# Start end end positions only has a value during evaluation.
if start_position_character is not None and not is_impossible:
# start_position is index of word, end_position inclusive
self.start_position = char_to_word_offset[start_position_character]
self.end_position = char_to_word_offset[
min(start_position_character + len(answer_text) - 1, len(char_to_word_offset) - 1)
]
def convert_examples_to_features(
examples: List[object],
tokenizer: object,
max_seq_length: int,
doc_stride: int,
max_query_length: int,
has_groundtruth: bool,
):
"""Loads a data file into a list of `InputBatch`s."""
unique_id = 1000000000
features = []
for (example_index, example) in enumerate(examples):
query_tokens = tokenizer.text_to_tokens(example.question_text)
if len(query_tokens) > max_query_length:
query_tokens = query_tokens[0:max_query_length]
# context: index of token -> index of word
tok_to_orig_index = []
# context: index of word -> index of first token in token list
orig_to_tok_index = []
# context without white spaces after tokenization
all_doc_tokens = []
# doc tokens is word separated context
for (i, token) in enumerate(example.doc_tokens):
orig_to_tok_index.append(len(all_doc_tokens))
sub_tokens = tokenizer.text_to_tokens(token)
for sub_token in sub_tokens:
tok_to_orig_index.append(i)
all_doc_tokens.append(sub_token)
# idx of query token start and end in context
tok_start_position = None
tok_end_position = None
if has_groundtruth and example.is_impossible:
tok_start_position = -1
tok_end_position = -1
if has_groundtruth and not example.is_impossible:
tok_start_position = orig_to_tok_index[example.start_position]
if example.end_position < len(example.doc_tokens) - 1:
tok_end_position = orig_to_tok_index[example.end_position + 1] - 1
else:
tok_end_position = len(all_doc_tokens) - 1
(tok_start_position, tok_end_position) = _improve_answer_span(
all_doc_tokens, tok_start_position, tok_end_position, tokenizer, example.answer_text
)
# The -3 accounts for tokenizer.cls_token, tokenizer.sep_token and tokenizer.sep_token
# doc_spans contains all possible contexts options of given length
max_tokens_for_doc = max_seq_length - len(query_tokens) - 3
_DocSpan = collections.namedtuple("DocSpan", ["start", "length"])
doc_spans = []
start_offset = 0
while start_offset < len(all_doc_tokens):
length = len(all_doc_tokens) - start_offset
if length > max_tokens_for_doc:
length = max_tokens_for_doc
doc_spans.append(_DocSpan(start=start_offset, length=length))
if start_offset + length == len(all_doc_tokens):
break
start_offset += min(length, doc_stride)
for (doc_span_index, doc_span) in enumerate(doc_spans):
tokens = []
# maps context tokens idx in final input -> word idx in context
token_to_orig_map = {}
token_is_max_context = {}
segment_ids = []
tokens.append(tokenizer.cls_token)
segment_ids.append(0)
for token in query_tokens:
tokens.append(token)
segment_ids.append(0)
tokens.append(tokenizer.sep_token)
segment_ids.append(0)
for i in range(doc_span.length):
split_token_index = doc_span.start + i
token_to_orig_map[len(tokens)] = tok_to_orig_index[split_token_index]
is_max_context = _check_is_max_context(doc_spans, doc_span_index, split_token_index)
token_is_max_context[len(tokens)] = is_max_context
tokens.append(all_doc_tokens[split_token_index])
segment_ids.append(1)
tokens.append(tokenizer.sep_token)
segment_ids.append(1)
input_ids = tokenizer.tokens_to_ids(tokens)
# The mask has 1 for real tokens and 0 for padding tokens.
# Only real tokens are attended to.
input_mask = [1] * len(input_ids)
# Zero-pad up to the sequence length.
while len(input_ids) < max_seq_length:
input_ids.append(tokenizer.pad_id)
input_mask.append(0)
segment_ids.append(0)
assert len(input_ids) == max_seq_length
assert len(input_mask) == max_seq_length
assert len(segment_ids) == max_seq_length
# calculate start and end position in final array
# of tokens in answer if no answer,
# 0 for both pointing to tokenizer.cls_token
start_position = None
end_position = None
if has_groundtruth and not example.is_impossible:
doc_start = doc_span.start
doc_end = doc_span.start + doc_span.length - 1
out_of_span = False
if not (tok_start_position >= doc_start and tok_end_position <= doc_end):
out_of_span = True
if out_of_span:
start_position = 0
end_position = 0
else:
doc_offset = len(query_tokens) + 2
start_position = tok_start_position - doc_start + doc_offset
end_position = tok_end_position - doc_start + doc_offset
if has_groundtruth and example.is_impossible:
# if our document chunk does not contain
# an annotation we throw it out, since there is nothing
# to predict.
start_position = 0
end_position = 0
if example_index < 1:
logging.info("*** Example ***")
logging.info("unique_id: %s" % (unique_id))
logging.info("example_index: %s" % (example_index))
logging.info("doc_span_index: %s" % (doc_span_index))
logging.info("tokens: %s" % " ".join(tokens))
logging.info(
"token_to_orig_map: %s" % " ".join(["%d:%d" % (x, y) for (x, y) in token_to_orig_map.items()])
)
logging.info(
"token_is_max_context: %s"
% " ".join(["%d:%s" % (x, y) for (x, y) in token_is_max_context.items()])
)
logging.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
logging.info("input_mask: %s" % " ".join([str(x) for x in input_mask]))
logging.info("segment_ids: %s" % " ".join([str(x) for x in segment_ids]))
if has_groundtruth and example.is_impossible:
logging.info("impossible example")
if has_groundtruth and not example.is_impossible:
answer_text = " ".join(tokens[start_position : (end_position + 1)])
logging.info("start_position: %d" % (start_position))
logging.info("end_position: %d" % (end_position))
logging.info("answer: %s" % (answer_text))
if example_index % 100 == 0:
logging.info(f"Finished processing: {example_index}")
features.append(
InputFeatures(
unique_id=unique_id,
example_index=example_index,
doc_span_index=doc_span_index,
tokens=tokens,
token_to_orig_map=token_to_orig_map,
token_is_max_context=token_is_max_context,
input_ids=input_ids,
input_mask=input_mask,
segment_ids=segment_ids,
start_position=start_position,
end_position=end_position,
is_impossible=example.is_impossible,
)
)
unique_id += 1
return features
def _improve_answer_span(
doc_tokens: List[str], input_start: int, input_end: int, tokenizer: object, orig_answer_text: str
):
"""Returns tokenized answer spans that
better match the annotated answer."""
tok_answer_text = " ".join(tokenizer.text_to_tokens(orig_answer_text))
for new_start in range(input_start, input_end + 1):
for new_end in range(input_end, new_start - 1, -1):
text_span = " ".join(doc_tokens[new_start : (new_end + 1)])
if text_span == tok_answer_text:
return (new_start, new_end)
return (input_start, input_end)
def _check_is_max_context(doc_spans, cur_span_index, position):
"""Check if this is the 'max context' doc span for the token.
Because of the sliding window approach taken to scoring documents,
a single token can appear in multiple documents.
Example:
Doc: the man went to the store and bought a gallon of milk
Span A: the man went to the
Span B: to the store and bought
Span C: and bought a gallon of
...
Now the word 'bought' will have two scores from spans B and C. We only
want to consider the score with "maximum context", which we define as
the *minimum* of its left and right context (the *sum* of left and
right context will always be the same, of course).
In the example the maximum context for 'bought' would be span C since
it has 1 left context and 3 right context, while span B has 4 left context
and 0 right context.
Code adapted from the code by the Google AI and HuggingFace.
"""
best_score = None
best_span_index = None
for (span_index, doc_span) in enumerate(doc_spans):
end = doc_span.start + doc_span.length - 1
if position < doc_span.start:
continue
if position > end:
continue
num_left_context = position - doc_span.start
num_right_context = end - position
score = min(num_left_context, num_right_context) + 0.01 * doc_span.length
if best_score is None or score > best_score:
best_score = score
best_span_index = span_index
return cur_span_index == best_span_index
class InputFeatures(object):
"""A single set of features of data."""
def __init__(
self,
unique_id: int,
example_index: int,
doc_span_index: int,
tokens: List[str],
token_to_orig_map: Dict[int, int],
token_is_max_context: Dict[int, bool],
input_ids: List[int],
input_mask: List[int],
segment_ids: List[int],
start_position: Optional[int] = None,
end_position: Optional[int] = None,
is_impossible: Optional[int] = None,
):
self.unique_id = unique_id
self.example_index = example_index
self.doc_span_index = doc_span_index
self.tokens = tokens
self.token_to_orig_map = token_to_orig_map
self.token_is_max_context = token_is_max_context
self.input_ids = input_ids
self.input_mask = input_mask
self.segment_ids = segment_ids
self.start_position = start_position
self.end_position = end_position
self.is_impossible = is_impossible
@@ -0,0 +1,19 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.data.text_classification.text_classification_dataset import (
TextClassificationDataset,
calc_class_weights,
)
@@ -0,0 +1,297 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import pickle
import random
from typing import Dict, List, Optional
import numpy as np
import torch
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.collections.nlp.data.data_utils.data_preprocessing import (
fill_class_weights,
get_freq_weights,
get_label_stats,
get_stats,
)
from nemo.collections.nlp.parts.utils_funcs import list2str
from nemo.core.classes import Dataset
from nemo.core.neural_types import ChannelType, LabelsType, MaskType, NeuralType
from nemo.utils import logging
from nemo.utils.env_var_parsing import get_envint
__all__ = ['TextClassificationDataset', 'calc_class_weights']
class TextClassificationDataset(Dataset):
"""A dataset class that converts from raw data to
a dataset that can be used by DataLayerNM.
Args:
input_file: file to sequence + label.
the first line is header (sentence [tab] label)
each line should be [sentence][tab][label]
tokenizer: tokenizer object such as AutoTokenizer
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
num_samples: number of samples you want to use for the dataset.
If -1, use all dataset. Useful for testing.
shuffle: Shuffles the dataset after loading.
use_cache: Enables caching to use pickle format to store and read data from
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
'label': NeuralType(('B',), LabelsType()),
}
def __init__(
self,
tokenizer: TokenizerSpec,
input_file: str = None,
queries: List[str] = None,
max_seq_length: int = -1,
num_samples: int = -1,
shuffle: bool = False,
use_cache: bool = False,
):
if not input_file and not queries:
raise ValueError("Either input_file or queries should be passed to the text classification dataset.")
if input_file and not os.path.exists(input_file):
raise FileNotFoundError(
f'Data file `{input_file}` not found! Each line of the data file should contain text sequences, where '
f'words are separated with spaces and the label separated by [TAB] following this format: '
f'[WORD][SPACE][WORD][SPACE][WORD][TAB][LABEL]'
)
self.input_file = input_file
self.tokenizer = tokenizer
self.max_seq_length = max_seq_length
self.num_samples = num_samples
self.shuffle = shuffle
self.use_cache = use_cache
self.vocab_size = self.tokenizer.vocab_size
self.pad_id = tokenizer.pad_id
self.features = None
labels, all_sents = [], []
if input_file:
data_dir, filename = os.path.split(input_file)
vocab_size = getattr(tokenizer, "vocab_size", 0)
tokenizer_name = tokenizer.name
cached_features_file = os.path.join(
data_dir,
f"cached_{filename}_{tokenizer_name}_{max_seq_length}_{vocab_size}_{num_samples}_{self.pad_id}_{shuffle}.pkl",
)
if get_envint("LOCAL_RANK", 0) == 0:
if use_cache and os.path.exists(cached_features_file):
logging.warning(
f"Processing of {input_file} is skipped as caching is enabled and a cache file "
f"{cached_features_file} already exists."
)
logging.warning(
f"You may need to delete the cache file if any of the processing parameters (eg. tokenizer) or "
f"the data are updated."
)
else:
with open(input_file, "r") as f:
lines = f.readlines()
logging.info(f'Read {len(lines)} examples from {input_file}.')
if num_samples > 0:
lines = lines[:num_samples]
logging.warning(
f"Parameter 'num_samples' is set, so just the first {len(lines)} examples are kept."
)
if shuffle:
random.shuffle(lines)
for index, line in enumerate(lines):
if index % 20000 == 0:
logging.debug(f"Processing line {index}/{len(lines)}")
line_splited = line.strip().split()
try:
label = int(line_splited[-1])
except ValueError:
logging.debug(f"Skipping line {line}")
continue
labels.append(label)
sent_words = line_splited[:-1]
all_sents.append(sent_words)
verbose = True
self.features = self.get_features(
all_sents=all_sents,
tokenizer=tokenizer,
max_seq_length=max_seq_length,
labels=labels,
verbose=verbose,
)
with open(cached_features_file, 'wb') as out_file:
pickle.dump(self.features, out_file, protocol=pickle.HIGHEST_PROTOCOL)
else:
for query in queries:
all_sents.append(query.strip().split())
labels = [-1] * len(all_sents)
verbose = False
self.features = self.get_features(
all_sents=all_sents, tokenizer=tokenizer, max_seq_length=max_seq_length, labels=labels, verbose=verbose
)
# wait until the master process writes to the processed data files
if torch.distributed.is_initialized():
torch.distributed.barrier()
if input_file:
with open(cached_features_file, "rb") as input_file:
self.features = pickle.load(input_file)
def __len__(self):
return len(self.features)
def __getitem__(self, idx):
return self.features[idx]
def _collate_fn(self, batch):
"""collate batch of input_ids, segment_ids, input_mask, and label
Args:
batch: A list of tuples of (input_ids, segment_ids, input_mask, label).
"""
max_length = 0
for input_ids, segment_ids, input_mask, label in batch:
if len(input_ids) > max_length:
max_length = len(input_ids)
padded_input_ids = []
padded_segment_ids = []
padded_input_mask = []
labels = []
for input_ids, segment_ids, input_mask, label in batch:
if len(input_ids) < max_length:
pad_width = max_length - len(input_ids)
padded_input_ids.append(np.pad(input_ids, pad_width=[0, pad_width], constant_values=self.pad_id))
padded_segment_ids.append(np.pad(segment_ids, pad_width=[0, pad_width], constant_values=self.pad_id))
padded_input_mask.append(np.pad(input_mask, pad_width=[0, pad_width], constant_values=self.pad_id))
else:
padded_input_ids.append(input_ids)
padded_segment_ids.append(segment_ids)
padded_input_mask.append(input_mask)
labels.append(label)
return (
torch.LongTensor(padded_input_ids),
torch.LongTensor(padded_segment_ids),
torch.LongTensor(padded_input_mask),
torch.LongTensor(labels),
)
@staticmethod
def get_features(all_sents, tokenizer, max_seq_length, labels=None, verbose=True):
"""Encode a list of sentences into a list of tuples of (input_ids, segment_ids, input_mask, label)."""
features = []
sent_lengths = []
too_long_count = 0
for sent_id, sent in enumerate(all_sents):
if sent_id % 1000 == 0:
logging.debug(f"Encoding sentence {sent_id}/{len(all_sents)}")
sent_subtokens = [tokenizer.cls_token]
for word in sent:
word_tokens = tokenizer.text_to_tokens(word)
sent_subtokens.extend(word_tokens)
if max_seq_length > 0 and len(sent_subtokens) + 1 > max_seq_length:
sent_subtokens = sent_subtokens[:max_seq_length]
too_long_count += 1
sent_subtokens.append(tokenizer.sep_token)
sent_lengths.append(len(sent_subtokens))
input_ids = [tokenizer.tokens_to_ids(t) for t in sent_subtokens]
# The mask has 1 for real tokens and 0 for padding tokens.
# Only real tokens are attended to.
input_mask = [1] * len(input_ids)
segment_ids = [0] * len(input_ids)
if verbose and sent_id < 2:
logging.info("*** Example ***")
logging.info(f"example {sent_id}: {sent}")
logging.info("subtokens: %s" % " ".join(sent_subtokens))
logging.info("input_ids: %s" % list2str(input_ids))
logging.info("segment_ids: %s" % list2str(segment_ids))
logging.info("input_mask: %s" % list2str(input_mask))
logging.info("label: %s" % labels[sent_id] if labels else "**Not Provided**")
label = labels[sent_id] if labels else -1
features.append([np.asarray(input_ids), np.asarray(segment_ids), np.asarray(input_mask), label])
if max_seq_length > -1 and too_long_count > 0:
logging.warning(
f'Found {too_long_count} out of {len(all_sents)} sentences with more than {max_seq_length} subtokens. '
f'Truncated long sentences from the end.'
)
if verbose:
get_stats(sent_lengths)
return features
def calc_class_weights(file_path: str, num_classes: int):
"""
iterates over a data file and calculate the weights of each class to be used for class_balancing
Args:
file_path: path to the data file
num_classes: number of classes in the dataset
"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"Could not find data file {file_path} to calculate the class weights!")
with open(file_path, 'r') as f:
input_lines = f.readlines()
labels = []
for input_line in input_lines:
parts = input_line.strip().split()
try:
label = int(parts[-1])
except ValueError:
raise ValueError(
f'No numerical labels found for {file_path}. Labels should be integers and separated by [TAB] at the end of each line.'
)
labels.append(label)
logging.info(f'Calculating stats of {file_path}...')
total_sents, sent_label_freq, max_id = get_label_stats(labels, f'{file_path}_sentence_stats.tsv', verbose=False)
if max_id >= num_classes:
raise ValueError(f'Found an invalid label in {file_path}! Labels should be from [0, num_classes-1].')
class_weights_dict = get_freq_weights(sent_label_freq)
logging.info(f'Total Sentence: {total_sents}')
logging.info(f'Sentence class frequencies: {sent_label_freq}')
logging.info(f'Class Weights: {class_weights_dict}')
class_weights = fill_class_weights(weights=class_weights_dict, max_id=num_classes - 1)
return class_weights
@@ -0,0 +1,13 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,466 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = ['BertPunctuationCapitalizationDataset', 'BertPunctuationCapitalizationInferDataset']
import itertools
import os
import pickle
from typing import Dict, List, Optional
import numpy as np
import torch
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.collections.nlp.data.data_utils.data_preprocessing import get_label_stats, get_stats
from nemo.core.classes import Dataset
from nemo.core.neural_types import ChannelType, LabelsType, MaskType, NeuralType
from nemo.utils import logging
def get_features(
queries: List[str],
max_seq_length: int,
tokenizer: TokenizerSpec,
punct_label_ids: dict = None,
capit_label_ids: dict = None,
pad_label: str = 'O',
punct_labels_lines=None,
capit_labels_lines=None,
ignore_extra_tokens=False,
ignore_start_end: Optional[bool] = False,
):
"""
Processes the data and returns features.
Args:
queries: text sequences
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
tokenizer: such as AutoTokenizer
pad_label: pad value use for labels. By default, it's the neutral label.
punct_label_ids: dict to map punctuation labels to label ids.
Starts with pad_label->0 and then increases in alphabetical order.
Required for training and evaluation, not needed for inference.
capit_label_ids: dict to map labels to label ids. Starts
with pad_label->0 and then increases in alphabetical order.
Required for training and evaluation, not needed for inference.
punct_labels: list of labels for every word in a sequence (str)
capit_labels: list of labels for every word in a sequence (str)
ignore_extra_tokens: whether to ignore extra tokens in the loss_mask
ignore_start_end: whether to ignore bos and eos tokens in the loss_mask
Returns:
all_input_ids: input ids for all tokens
all_segment_ids: token type ids
all_input_mask: attention mask to use for BERT model
all_subtokens_mask: masks out all subwords besides the first one
all_loss_mask: loss mask to mask out tokens during training
punct_all_labels: all labels for punctuation task (ints)
capit_all_labels: all labels for capitalization task (ints)
punct_label_ids: label (str) to id (int) map for punctuation task
capit_label_ids: label (str) to id (int) map for capitalization task
"""
all_subtokens = []
all_loss_mask = []
all_subtokens_mask = []
all_segment_ids = []
all_input_ids = []
all_input_mask = []
sent_lengths = []
punct_all_labels = []
capit_all_labels = []
with_label = False
if punct_labels_lines and capit_labels_lines:
with_label = True
for i, query in enumerate(queries):
words = query.strip().split()
# add bos token
subtokens = [tokenizer.cls_token]
loss_mask = [1 - ignore_start_end]
subtokens_mask = [0]
if with_label:
pad_id = punct_label_ids[pad_label]
punct_labels = [pad_id]
punct_query_labels = [punct_label_ids[lab] for lab in punct_labels_lines[i]]
capit_labels = [pad_id]
capit_query_labels = [capit_label_ids[lab] for lab in capit_labels_lines[i]]
for j, word in enumerate(words):
word_tokens = tokenizer.text_to_tokens(word)
subtokens.extend(word_tokens)
loss_mask.append(1)
loss_mask.extend([int(not ignore_extra_tokens)] * (len(word_tokens) - 1))
subtokens_mask.append(1)
subtokens_mask.extend([0] * (len(word_tokens) - 1))
if with_label:
punct_labels.extend([punct_query_labels[j]] * len(word_tokens))
capit_labels.extend([capit_query_labels[j]] * len(word_tokens))
# add eos token
subtokens.append(tokenizer.sep_token)
loss_mask.append(1 - ignore_start_end)
subtokens_mask.append(0)
sent_lengths.append(len(subtokens))
all_subtokens.append(subtokens)
all_loss_mask.append(loss_mask)
all_subtokens_mask.append(subtokens_mask)
all_input_mask.append([1] * len(subtokens))
if with_label:
punct_labels.append(pad_id)
punct_all_labels.append(punct_labels)
capit_labels.append(pad_id)
capit_all_labels.append(capit_labels)
max_seq_length = min(max_seq_length, max(sent_lengths))
logging.info(f'Max length: {max_seq_length}')
get_stats(sent_lengths)
too_long_count = 0
for i, subtokens in enumerate(all_subtokens):
if len(subtokens) > max_seq_length:
subtokens = [tokenizer.cls_token] + subtokens[-max_seq_length + 1 :]
all_input_mask[i] = [1] + all_input_mask[i][-max_seq_length + 1 :]
all_loss_mask[i] = [int(not ignore_start_end)] + all_loss_mask[i][-max_seq_length + 1 :]
all_subtokens_mask[i] = [0] + all_subtokens_mask[i][-max_seq_length + 1 :]
if with_label:
punct_all_labels[i] = [pad_id] + punct_all_labels[i][-max_seq_length + 1 :]
capit_all_labels[i] = [pad_id] + capit_all_labels[i][-max_seq_length + 1 :]
too_long_count += 1
all_input_ids.append(tokenizer.tokens_to_ids(subtokens))
if len(subtokens) < max_seq_length:
extra = max_seq_length - len(subtokens)
all_input_ids[i] = all_input_ids[i] + [0] * extra
all_loss_mask[i] = all_loss_mask[i] + [0] * extra
all_subtokens_mask[i] = all_subtokens_mask[i] + [0] * extra
all_input_mask[i] = all_input_mask[i] + [0] * extra
if with_label:
punct_all_labels[i] = punct_all_labels[i] + [pad_id] * extra
capit_all_labels[i] = capit_all_labels[i] + [pad_id] * extra
all_segment_ids.append([0] * max_seq_length)
logging.info(f'{too_long_count} are longer than {max_seq_length}')
for i in range(min(len(all_input_ids), 5)):
logging.info("*** Example ***")
logging.info("i: %s" % (i))
logging.info("subtokens: %s" % " ".join(list(map(str, all_subtokens[i]))))
logging.info("loss_mask: %s" % " ".join(list(map(str, all_loss_mask[i]))))
logging.info("input_mask: %s" % " ".join(list(map(str, all_input_mask[i]))))
logging.info("subtokens_mask: %s" % " ".join(list(map(str, all_subtokens_mask[i]))))
if with_label:
logging.info("punct_labels: %s" % " ".join(list(map(str, punct_all_labels[i]))))
logging.info("capit_labels: %s" % " ".join(list(map(str, capit_all_labels[i]))))
return (
all_input_ids,
all_segment_ids,
all_input_mask,
all_subtokens_mask,
all_loss_mask,
punct_all_labels,
capit_all_labels,
punct_label_ids,
capit_label_ids,
)
class BertPunctuationCapitalizationDataset(Dataset):
"""
Creates dataset to use during training for punctuaion and capitalization tasks with a pretrained model.
For dataset to use during inference without labels, see BertPunctuationCapitalizationInferDataset.
Args:
text_file: file to sequences, each line should a sentence, no header.
label_file: file to labels, each line corresponds to word labels for a sentence in the text_file. No header.
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
tokenizer: such as AutoTokenizer
num_samples: number of samples you want to use for the dataset.
If -1, use all dataset. Useful for testing.
pad_label: pad value use for labels.
by default, it's the neutral label.
punct_label_ids and capit_label_ids (dict):
dict to map labels to label ids.
Starts with pad_label->0 and then increases in alphabetical order
For dev set use label_ids generated during training to support
cases when not all labels are present in the dev set.
For training set label_ids should be None or loaded from cache
ignore_extra_tokens: whether to ignore extra tokens in the loss_mask
ignore_start_end: whether to ignore bos and eos tokens in the loss_mask
use_cache: whether to use processed data cache or not
get_label_frequencies: whether to generate label frequencies
punct_label_ids_file and capit_label_ids_file: name of the files to save in .nemo
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports. """
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
'subtokens_mask': NeuralType(('B', 'T'), MaskType()),
'loss_mask': NeuralType(('B', 'T'), MaskType()),
'punct_labels': NeuralType(('B', 'T'), LabelsType()),
'capit_labels': NeuralType(('B', 'T'), LabelsType()),
}
def __init__(
self,
text_file: str,
label_file: str,
max_seq_length: int,
tokenizer: TokenizerSpec,
num_samples: int = -1,
pad_label: str = 'O',
punct_label_ids: Dict[str, int] = None,
capit_label_ids: Dict[str, int] = None,
ignore_extra_tokens: bool = False,
ignore_start_end: bool = False,
use_cache: bool = True,
get_label_frequencies: bool = False,
punct_label_ids_file: str = 'punct_label_ids.csv',
capit_label_ids_file: str = 'capit_label_ids.csv',
):
""" Initializes BertPunctuationCapitalizationDataset. """
if not (os.path.exists(text_file) and os.path.exists(label_file)):
raise FileNotFoundError(
f'{text_file} or {label_file} not found. The data should be splitted into 2 files: text.txt and \
labels.txt. Each line of the text.txt file contains text sequences, where words are separated with \
spaces. The labels.txt file contains corresponding labels for each word in text.txt, the labels are \
separated with spaces. Each line of the files should follow the format: \
[WORD] [SPACE] [WORD] [SPACE] [WORD] (for text.txt) and \
[LABEL] [SPACE] [LABEL] [SPACE] [LABEL] (for labels.txt).'
)
# Cache features
data_dir = os.path.dirname(text_file)
filename = os.path.basename(text_file)
if not filename.endswith('.txt'):
raise ValueError("{text_file} should have extension .txt")
filename = filename[:-4]
vocab_size = getattr(tokenizer, "vocab_size", 0)
features_pkl = os.path.join(
data_dir,
"cached_{}_{}_{}_{}_{}".format(
filename, tokenizer.name, str(max_seq_length), str(vocab_size), str(num_samples)
),
)
self.punct_label_ids_file = os.path.join(data_dir, punct_label_ids_file)
self.capit_label_ids_file = os.path.join(data_dir, capit_label_ids_file)
master_device = not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0
cache_files_exist = (
os.path.exists(features_pkl)
and os.path.exists(self.punct_label_ids_file)
and os.path.exists(self.capit_label_ids_file)
)
features = None
if master_device and not (cache_files_exist and use_cache):
if num_samples == 0:
raise ValueError("num_samples has to be positive", num_samples)
logging.info(f'Processing {text_file}')
with open(text_file, 'r') as f:
text_lines = f.readlines()
# Collect all possible labels
punct_unique_labels = set()
capit_unique_labels = set()
punct_labels_lines = []
capit_labels_lines = []
with open(label_file, 'r') as f:
for line in f:
line = line.strip().split()
# extract punctuation and capitalization labels
punct_line, capit_line = zip(*line)
punct_labels_lines.append(punct_line)
capit_labels_lines.append(capit_line)
punct_unique_labels.update(punct_line)
capit_unique_labels.update(capit_line)
if len(punct_labels_lines) != len(text_lines):
raise ValueError("Labels file should contain labels for every word")
dataset = list(zip(text_lines, punct_labels_lines, capit_labels_lines))
if num_samples > 0:
dataset = dataset[:num_samples]
dataset = list(zip(*dataset))
text_lines = dataset[0]
punct_labels_lines = dataset[1]
capit_labels_lines = dataset[2]
# for dev/test sets use label mapping from training set
if punct_label_ids:
if len(punct_label_ids) != len(punct_unique_labels):
logging.info(
'Not all labels from the specified'
+ 'label_ids dictionary are present in the'
+ 'current dataset. Using the provided'
+ 'label_ids dictionary.'
)
else:
logging.info('Using the provided label_ids dictionary.')
else:
logging.info(
'Creating a new label to label_id dictionary.'
+ ' It\'s recommended to use label_ids generated'
+ ' during training for dev/test sets to avoid'
+ ' errors if some labels are not'
+ ' present in the dev/test sets.'
+ ' For training set label_ids should be None.'
)
def create_label_ids(unique_labels, pad_label=pad_label):
label_ids = {pad_label: 0}
if pad_label in unique_labels:
unique_labels.remove(pad_label)
for label in sorted(unique_labels):
label_ids[label] = len(label_ids)
return label_ids
punct_label_ids = create_label_ids(punct_unique_labels)
capit_label_ids = create_label_ids(capit_unique_labels)
self._save_label_ids(punct_label_ids, self.punct_label_ids_file)
self._save_label_ids(capit_label_ids, self.capit_label_ids_file)
features = get_features(
text_lines,
max_seq_length,
tokenizer,
pad_label=pad_label,
punct_labels_lines=punct_labels_lines,
capit_labels_lines=capit_labels_lines,
punct_label_ids=punct_label_ids,
capit_label_ids=capit_label_ids,
ignore_extra_tokens=ignore_extra_tokens,
ignore_start_end=ignore_start_end,
)
pickle.dump(features, open(features_pkl, "wb"))
logging.info(f'Features saved to {features_pkl}')
# wait until the master process writes to the processed data files
if torch.distributed.is_initialized():
torch.distributed.barrier()
if features is None:
features = pickle.load(open(features_pkl, 'rb'))
logging.info(f'Features restored from {features_pkl}')
self.all_input_ids = features[0]
self.all_segment_ids = features[1]
self.all_input_mask = features[2]
self.all_subtokens_mask = features[3]
self.all_loss_mask = features[4]
self.punct_all_labels = features[5]
self.capit_all_labels = features[6]
self.punct_label_ids = features[7]
self.capit_label_ids = features[8]
if get_label_frequencies:
self.punct_label_frequencies = self._calculate_label_frequencies(self.punct_all_labels, data_dir, 'punct')
self.capit_label_frequencies = self._calculate_label_frequencies(self.capit_all_labels, data_dir, 'capit')
def _calculate_label_frequencies(self, all_labels: List[int], data_dir: str, name: str) -> Dict[str, float]:
""" Calculates labels frequencies """
merged_labels = itertools.chain.from_iterable(all_labels)
logging.info('Three most popular labels')
_, label_frequencies, _ = get_label_stats(merged_labels, data_dir + '/label_count_' + name + '.tsv')
return label_frequencies
def _save_label_ids(self, label_ids: Dict[str, int], filename: str) -> None:
""" Saves label ids map to a file """
with open(filename, 'w') as out:
labels, _ = zip(*sorted(label_ids.items(), key=lambda x: x[1]))
out.write('\n'.join(labels))
logging.info(f'Labels: {label_ids}')
logging.info(f'Labels mapping saved to : {out.name}')
def __len__(self):
return len(self.all_input_ids)
def __getitem__(self, idx):
return (
np.array(self.all_input_ids[idx]),
np.array(self.all_segment_ids[idx]),
np.array(self.all_input_mask[idx], dtype=np.long),
np.array(self.all_subtokens_mask[idx]),
np.array(self.all_loss_mask[idx]),
np.array(self.punct_all_labels[idx]),
np.array(self.capit_all_labels[idx]),
)
class BertPunctuationCapitalizationInferDataset(Dataset):
"""
Creates dataset to use during inference for punctuation and capitalization tasks with a pretrained model.
For dataset to use during training with labels, see BertPunctuationCapitalizationDataset.
Args:
queries file to sequences, each line should a sentence, no header.
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
tokenizer: such as AutoTokenizer
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
'subtokens_mask': NeuralType(('B', 'T'), MaskType()),
}
def __init__(self, queries: List[str], max_seq_length: int, tokenizer: TokenizerSpec):
""" Initializes BertPunctuationCapitalizationInferDataset. """
features = get_features(queries=queries, max_seq_length=max_seq_length, tokenizer=tokenizer)
self.all_input_ids = features[0]
self.all_segment_ids = features[1]
self.all_input_mask = features[2]
self.all_subtokens_mask = features[3]
def __len__(self):
return len(self.all_input_ids)
def __getitem__(self, idx):
return (
np.array(self.all_input_ids[idx]),
np.array(self.all_segment_ids[idx]),
np.array(self.all_input_mask[idx], dtype=np.float32),
np.array(self.all_subtokens_mask[idx]),
)
@@ -0,0 +1,338 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Utility functions for Token Classification NLP tasks
Some parts of this code were adapted from the HuggingFace library at
https://github.com/huggingface/pytorch-pretrained-BERT
"""
import os
import pickle
from typing import Dict, List, Optional
import numpy as np
import torch
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.collections.nlp.data.data_utils.data_preprocessing import get_stats
from nemo.core.classes import Dataset
from nemo.core.neural_types import ChannelType, LabelsType, MaskType, NeuralType
from nemo.utils import logging
__all__ = ['BertTokenClassificationDataset', 'BertTokenClassificationInferDataset']
def get_features(
queries: List[str],
tokenizer: TokenizerSpec,
max_seq_length: int = -1,
label_ids: dict = None,
pad_label: str = 'O',
raw_labels: List[str] = None,
ignore_extra_tokens: bool = False,
ignore_start_end: bool = False,
):
"""
Processes the data and returns features.
Args:
queries: text sequences
tokenizer: such as AutoTokenizer
max_seq_length: max sequence length minus 2 for [CLS] and [SEP], when -1 - use the max len from the data
pad_label: pad value use for labels. By default, it's the neutral label.
raw_labels: list of labels for every word in a sequence
label_ids: dict to map labels to label ids.
Starts with pad_label->0 and then increases in alphabetical order.
Required for training and evaluation, not needed for inference.
ignore_extra_tokens: whether to ignore extra tokens in the loss_mask
ignore_start_end: whether to ignore bos and eos tokens in the loss_mask
"""
all_subtokens = []
all_loss_mask = []
all_subtokens_mask = []
all_segment_ids = []
all_input_ids = []
all_input_mask = []
sent_lengths = []
all_labels = []
with_label = False
if raw_labels is not None:
with_label = True
for i, query in enumerate(queries):
words = query.strip().split()
# add bos token
subtokens = [tokenizer.cls_token]
loss_mask = [1 - ignore_start_end]
subtokens_mask = [0]
if with_label:
pad_id = label_ids[pad_label]
labels = [pad_id]
query_labels = [label_ids[lab] for lab in raw_labels[i]]
for j, word in enumerate(words):
word_tokens = tokenizer.text_to_tokens(word)
subtokens.extend(word_tokens)
loss_mask.append(1)
loss_mask.extend([int(not ignore_extra_tokens)] * (len(word_tokens) - 1))
subtokens_mask.append(1)
subtokens_mask.extend([0] * (len(word_tokens) - 1))
if with_label:
labels.extend([query_labels[j]] * len(word_tokens))
# add eos token
subtokens.append(tokenizer.sep_token)
loss_mask.append(1 - ignore_start_end)
subtokens_mask.append(0)
sent_lengths.append(len(subtokens))
all_subtokens.append(subtokens)
all_loss_mask.append(loss_mask)
all_subtokens_mask.append(subtokens_mask)
all_input_mask.append([1] * len(subtokens))
if with_label:
labels.append(pad_id)
all_labels.append(labels)
max_seq_length_data = max(sent_lengths)
max_seq_length = min(max_seq_length, max_seq_length_data) if max_seq_length > 0 else max_seq_length_data
logging.info(f'Setting Max Seq length to: {max_seq_length}')
get_stats(sent_lengths)
too_long_count = 0
for i, subtokens in enumerate(all_subtokens):
if len(subtokens) > max_seq_length:
subtokens = [tokenizer.cls_token] + subtokens[-max_seq_length + 1 :]
all_input_mask[i] = [1] + all_input_mask[i][-max_seq_length + 1 :]
all_loss_mask[i] = [int(not ignore_start_end)] + all_loss_mask[i][-max_seq_length + 1 :]
all_subtokens_mask[i] = [0] + all_subtokens_mask[i][-max_seq_length + 1 :]
if with_label:
all_labels[i] = [pad_id] + all_labels[i][-max_seq_length + 1 :]
too_long_count += 1
all_input_ids.append(tokenizer.tokens_to_ids(subtokens))
if len(subtokens) < max_seq_length:
extra = max_seq_length - len(subtokens)
all_input_ids[i] = all_input_ids[i] + [0] * extra
all_loss_mask[i] = all_loss_mask[i] + [0] * extra
all_subtokens_mask[i] = all_subtokens_mask[i] + [0] * extra
all_input_mask[i] = all_input_mask[i] + [0] * extra
if with_label:
all_labels[i] = all_labels[i] + [pad_id] * extra
all_segment_ids.append([0] * max_seq_length)
logging.warning(f'{too_long_count} are longer than {max_seq_length}')
for i in range(min(len(all_input_ids), 1)):
logging.info("*** Example ***")
logging.info("i: %s", i)
logging.info("subtokens: %s", " ".join(list(map(str, all_subtokens[i]))))
logging.info("loss_mask: %s", " ".join(list(map(str, all_loss_mask[i]))))
logging.info("input_mask: %s", " ".join(list(map(str, all_input_mask[i]))))
logging.info("subtokens_mask: %s", " ".join(list(map(str, all_subtokens_mask[i]))))
if with_label:
logging.info("labels: %s", " ".join(list(map(str, all_labels[i]))))
return (all_input_ids, all_segment_ids, all_input_mask, all_subtokens_mask, all_loss_mask, all_labels)
class BertTokenClassificationDataset(Dataset):
"""
Creates dataset to use during training for token classification tasks with a pretrained model.
Converts from raw data to an instance that can be used by Dataloader.
For dataset to use during inference without labels, see BertTokenClassificationInferDataset.
Args:
text_file: file to sequences, each line should a sentence, no header.
label_file: file to labels, each line corresponds to word labels for a sentence in the text_file. No header.
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
tokenizer: such as AutoTokenizer
num_samples: number of samples you want to use for the dataset.
If -1, use all dataset. Useful for testing.
pad_label: pad value use for labels. By default, it's the neutral label.
label_ids: label_ids (dict): dict to map labels to label ids.
Starts with pad_label->0 and then increases in alphabetical order
For dev set use label_ids generated during training to support
cases when not all labels are present in the dev set.
For training set label_ids should be None.
ignore_extra_tokens: whether to ignore extra tokens in the loss_mask
ignore_start_end: whether to ignore bos and eos tokens in the loss_mask
use_cache: whether to use processed data cache or not
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
'subtokens_mask': NeuralType(('B', 'T'), MaskType()),
'loss_mask': NeuralType(('B', 'T'), MaskType()),
'labels': NeuralType(('B', 'T'), LabelsType()),
}
def __init__(
self,
text_file: str,
label_file: str,
max_seq_length: int,
tokenizer: TokenizerSpec,
num_samples: int = -1,
pad_label: str = 'O',
label_ids: Dict[str, int] = None,
ignore_extra_tokens: bool = False,
ignore_start_end: bool = False,
use_cache: bool = True,
):
""" Initializes BertTokenClassificationDataset. """
data_dir = os.path.dirname(text_file)
filename = os.path.basename(text_file)
if not filename.endswith('.txt'):
raise ValueError("{text_file} should have extension .txt")
vocab_size = getattr(tokenizer, "vocab_size", 0)
features_pkl = os.path.join(
data_dir,
"cached_{}_{}_{}_{}_{}".format(
filename, tokenizer.name, str(max_seq_length), str(vocab_size), str(num_samples)
),
)
master_device = not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0
features = None
if master_device and (not use_cache or not os.path.exists(features_pkl)):
if num_samples == 0:
raise ValueError("num_samples has to be positive", num_samples)
with open(text_file, 'r') as f:
text_lines = f.readlines()
labels_lines = []
with open(label_file, 'r') as f:
for line in f:
line = line.strip().split()
labels_lines.append(line)
if len(labels_lines) != len(text_lines):
raise ValueError("Labels file should contain labels for every word")
if num_samples > 0:
dataset = list(zip(text_lines, labels_lines))
dataset = dataset[:num_samples]
dataset = list(zip(*dataset))
text_lines = dataset[0]
labels_lines = dataset[1]
features = get_features(
queries=text_lines,
max_seq_length=max_seq_length,
tokenizer=tokenizer,
pad_label=pad_label,
raw_labels=labels_lines,
label_ids=label_ids,
ignore_extra_tokens=ignore_extra_tokens,
ignore_start_end=ignore_start_end,
)
pickle.dump(features, open(features_pkl, "wb"))
logging.info(f'features saved to {features_pkl}')
# wait until the master process writes to the processed data files
if torch.distributed.is_initialized():
torch.distributed.barrier()
if features is None:
features = pickle.load(open(features_pkl, 'rb'))
logging.info(f'features restored from {features_pkl}')
self.all_input_ids = features[0]
self.all_segment_ids = features[1]
self.all_input_mask = features[2]
self.all_subtokens_mask = features[3]
self.all_loss_mask = features[4]
self.all_labels = features[5]
def __len__(self):
return len(self.all_input_ids)
def __getitem__(self, idx):
return (
np.array(self.all_input_ids[idx]),
np.array(self.all_segment_ids[idx]),
np.array(self.all_input_mask[idx], dtype=np.long),
np.array(self.all_subtokens_mask[idx]),
np.array(self.all_loss_mask[idx]),
np.array(self.all_labels[idx]),
)
class BertTokenClassificationInferDataset(Dataset):
"""
Creates dataset to use during inference for token classification tasks with a pretrained model.
For dataset to use during training with labels, see BertTokenClassificationDataset.
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
return {
'input_ids': NeuralType(('B', 'T'), ChannelType()),
'segment_ids': NeuralType(('B', 'T'), ChannelType()),
'input_mask': NeuralType(('B', 'T'), MaskType()),
'subtokens_mask': NeuralType(('B', 'T'), MaskType()),
}
def __init__(
self, queries: List[str], max_seq_length: int, tokenizer: TokenizerSpec,
):
"""
Initializes BertTokenClassificationInferDataset
Args:
queries: text sequences
max_seq_length: max sequence length minus 2 for [CLS] and [SEP]
tokenizer: such as AutoTokenizer
"""
features = get_features(queries=queries, max_seq_length=max_seq_length, tokenizer=tokenizer)
self.all_input_ids = features[0]
self.all_segment_ids = features[1]
self.all_input_mask = features[2]
self.all_subtokens_mask = features[3]
def __len__(self):
return len(self.all_input_ids)
def __getitem__(self, idx):
return (
np.array(self.all_input_ids[idx]),
np.array(self.all_segment_ids[idx]),
np.array(self.all_input_mask[idx], dtype=np.long),
np.array(self.all_subtokens_mask[idx]),
)
@@ -0,0 +1,118 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import pickle
from typing import Dict
from nemo.collections.nlp.data.data_utils.data_preprocessing import (
fill_class_weights,
get_freq_weights,
get_label_stats,
)
from nemo.utils import logging
__all__ = ['get_label_ids']
def get_label_ids(
label_file: str,
is_training: bool = False,
pad_label: str = 'O',
label_ids_dict: Dict[str, int] = None,
get_weights: bool = True,
class_labels_file_artifact='label_ids.csv',
):
"""
Generates str to int labels mapping for training data or checks correctness of the label_ids_dict
file for non-training files or if label_ids_dict is specified
Args:
label_file: the path of the label file to process
is_training: indicates whether the label_file is used for training
pad_label: token used for padding
label_ids_dict: str label name to int ids mapping. Required for non-training data.
If specified, the check that all labels from label_file are present in label_ids_dict will be performed.
For training data, if label_ids_dict is None, a new mapping will be generated from label_file.
get_weights: set to True to calculate class weights, required for Weighted Loss.
class_labels_file_artifact: name of the file to save in .nemo
"""
if not os.path.exists(label_file):
raise ValueError(f'File {label_file} was not found.')
logging.info(f'Processing {label_file}')
if not is_training and label_ids_dict is None:
raise ValueError(
f'For non training data, label_ids_dict created during preprocessing of the training data '
f'should be provided'
)
# collect all labels from the label_file
data_dir = os.path.dirname(label_file)
unique_labels = set(pad_label)
all_labels = []
with open(label_file, 'r') as f:
for line in f:
line = line.strip().split()
all_labels.extend(line)
unique_labels.update(line)
# check that all labels from label_file are present in the specified label_ids_dict
# or generate label_ids_dict from data (for training only)
if label_ids_dict:
logging.info(f'Using provided labels mapping {label_ids_dict}')
for name in unique_labels:
if name not in label_ids_dict:
raise ValueError(f'{name} class from {label_file} not found in the provided mapping: {label_ids_dict}')
else:
label_ids_dict = {pad_label: 0}
if pad_label in unique_labels:
unique_labels.remove(pad_label)
for label in sorted(unique_labels):
label_ids_dict[label] = len(label_ids_dict)
label_ids_filename = os.path.join(data_dir, class_labels_file_artifact)
if is_training:
with open(label_ids_filename, 'w') as f:
labels, _ = zip(*sorted(label_ids_dict.items(), key=lambda x: x[1]))
f.write('\n'.join(labels))
logging.info(f'Labels mapping {label_ids_dict} saved to : {label_ids_filename}')
# calculate label statistics
base_name = os.path.splitext(os.path.basename(label_file))[0]
stats_file = os.path.join(data_dir, f'{base_name}_label_stats.tsv')
if os.path.exists(stats_file) and not is_training and not get_weights:
logging.info(f'{stats_file} found, skipping stats calculation.')
else:
all_labels = [label_ids_dict[label] for label in all_labels]
logging.info(f'Three most popular labels in {label_file}:')
total_labels, label_frequencies, max_id = get_label_stats(all_labels, stats_file)
logging.info(f'Total labels: {total_labels}. Label frequencies - {label_frequencies}')
if get_weights:
class_weights_pkl = os.path.join(data_dir, f'{base_name}_weights.p')
if os.path.exists(class_weights_pkl):
class_weights = pickle.load(open(class_weights_pkl, 'rb'))
logging.info(f'Class weights restored from {class_weights_pkl}')
else:
class_weights_dict = get_freq_weights(label_frequencies)
logging.info(f'Class Weights: {class_weights_dict}')
class_weights = fill_class_weights(class_weights_dict, max_id)
pickle.dump(class_weights, open(class_weights_pkl, "wb"))
logging.info(f'Class weights saved to {class_weights_pkl}')
else:
class_weights = None
return label_ids_dict, label_ids_filename, class_weights
@@ -0,0 +1,15 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.losses.sgd_loss import SGDDialogueStateLoss
+218
View File
@@ -0,0 +1,218 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/baseline/train_and_predict.py
'''
import torch
from nemo.core.classes import Loss, Typing, typecheck
from nemo.core.neural_types import ChannelType, LabelsType, LogitsType, LossType, NeuralType
from nemo.utils import logging
__all__ = ['SGDDialogueStateLoss']
class SGDDialogueStateLoss(Loss, Typing):
"""
Neural module which implements loss for SGD model.
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
logit_intent_status: Output of SGD model
intent_status: intent label
logit_req_slot_status: Output of SGD model
requested_slot_status: Takes value 1 if the corresponding slot is requested, 0 otherwise
logit_cat_slot_status: Output of SGD model
categorical_slot_status: The status of each categorical slot in the service
logit_cat_slot_value_status: Output of SGD model
categorical_slot_value_status: Takes value 1 if the corresponding slot value is correct, 0 otherwise
logit_noncat_slot_status: Output of SGD model
noncategorical_slot_status: The status of each noncategorical slot in the service\
logit_spans: Output of SGD model
noncategorical_slot_value_start: The index of the starting subword corresponding to the slot span for a non-categorical slot value
noncategorical_slot_value_end: The index of the ending (inclusive) subword corresponding to the slot span for a non-categorical slot value
task_mask: Mask contains 1 if its the current task, 0 otherwise
"""
return {
"logit_intent_status": NeuralType(('B', 'T'), LogitsType()),
"intent_status": NeuralType(('B'), LabelsType()),
"logit_req_slot_status": NeuralType(('B', 'T'), LogitsType()),
"requested_slot_status": NeuralType(('B'), LabelsType()),
"logit_cat_slot_status": NeuralType(('B', 'T'), LogitsType()),
"categorical_slot_status": NeuralType(('B'), LabelsType()),
"logit_cat_slot_value_status": NeuralType(('B', 'T'), LogitsType()),
"categorical_slot_value_status": NeuralType(('B'), LabelsType()),
"logit_noncat_slot_status": NeuralType(('B', 'T'), LogitsType()),
"noncategorical_slot_status": NeuralType(('B'), LabelsType()),
"logit_spans": NeuralType(('B', 'T', 'D'), LogitsType()),
"noncategorical_slot_value_start": NeuralType(('B'), LabelsType()),
"noncategorical_slot_value_end": NeuralType(('B'), LabelsType()),
"task_mask": NeuralType(('B', 'T'), ChannelType()),
}
@property
def output_types(self):
"""
Returns definitions of module output ports.
loss:
NeuralType(None)
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, reduction: str = 'mean'):
"""
Args:
reduction: specifies the reduction to apply to the final loss, choose 'mean' or 'sum'
"""
super().__init__()
if reduction not in ['mean', 'sum']:
logging.warning(f'{reduction} reduction is not supported. Setting reduction to "mean"')
reduction = 'mean'
self.reduction = reduction
self._cross_entropy = torch.nn.CrossEntropyLoss(reduction=self.reduction)
self._cross_entropy_bin = torch.nn.BCEWithLogitsLoss(reduction=self.reduction)
def _helper(self, logits, labels, loss_mask=None):
"""
flattens logits and labels according loss mask
Args:
logits: logits
labels: labels
loss_mask: loss mask
Returns:
logits_flatten: flattened logits where loss mask is true
labels_flatten: flattened labels where loss mask is true
"""
logits_flatten = torch.flatten(logits, start_dim=0, end_dim=-2)
labels_flatten = torch.flatten(labels, start_dim=0, end_dim=-1)
if loss_mask is not None:
if loss_mask.dtype is not torch.bool:
loss_mask = loss_mask > 0.5
loss_mask_flatten = torch.flatten(loss_mask, start_dim=0, end_dim=-1)
logits_flatten = logits_flatten[loss_mask_flatten]
labels_flatten = labels_flatten[loss_mask_flatten]
return logits_flatten, labels_flatten
@typecheck()
def forward(
self,
logit_intent_status,
intent_status,
logit_req_slot_status,
requested_slot_status,
logit_cat_slot_status,
categorical_slot_status,
logit_cat_slot_value_status,
categorical_slot_value_status,
logit_noncat_slot_status,
noncategorical_slot_status,
logit_spans,
noncategorical_slot_value_start,
noncategorical_slot_value_end,
task_mask,
):
# Intent loss
old_logit_intent_status = logit_intent_status
logit_intent_status, intent_status = self._helper(logit_intent_status, intent_status, task_mask[:, 0])
if len(intent_status) == 0:
intent_loss = torch.clamp(torch.max(old_logit_intent_status.view(-1)), 0, 0)
else:
intent_loss = self._cross_entropy_bin(logit_intent_status.squeeze(dim=-1), intent_status)
old_logit_req_slot_status = logit_req_slot_status
logit_req_slot_status, requested_slot_status = self._helper(
logit_req_slot_status, requested_slot_status, task_mask[:, 1]
)
if len(requested_slot_status) == 0:
requested_slot_loss = torch.clamp(torch.max(old_logit_req_slot_status.view(-1)), 0, 0)
else:
requested_slot_loss = self._cross_entropy_bin(logit_req_slot_status.squeeze(dim=-1), requested_slot_status)
old_logit_cat_slot_status = logit_cat_slot_status
logit_cat_slot_status, categorical_slot_status = self._helper(
logit_cat_slot_status, categorical_slot_status, task_mask[:, 2]
)
if len(categorical_slot_status) == 0:
cat_slot_status_loss = torch.clamp(torch.max(old_logit_cat_slot_status.view(-1)), 0, 0)
else:
cat_slot_status_loss = self._cross_entropy(logit_cat_slot_status, categorical_slot_status,)
old_logit_cat_slot_value_status = logit_cat_slot_value_status
logit_cat_slot_value_status, categorical_slot_value_status = self._helper(
logit_cat_slot_value_status, categorical_slot_value_status, task_mask[:, 3]
)
if len(categorical_slot_value_status) == 0:
cat_slot_value_status_loss = torch.clamp(torch.max(old_logit_cat_slot_value_status.view(-1)), 0, 0)
else:
cat_slot_value_status_loss = self._cross_entropy_bin(
logit_cat_slot_value_status.squeeze(dim=-1), categorical_slot_value_status
)
old_logit_noncat_slot_status = logit_noncat_slot_status
logit_noncat_slot_status, noncategorical_slot_status = self._helper(
logit_noncat_slot_status, noncategorical_slot_status, task_mask[:, 4]
)
if len(noncategorical_slot_status) == 0:
noncat_slot_status_loss = torch.clamp(torch.max(old_logit_noncat_slot_status.view(-1)), 0, 0)
else:
noncat_slot_status_loss = self._cross_entropy(logit_noncat_slot_status, noncategorical_slot_status,)
logit_noncat_slot_start, logit_noncat_slot_end = torch.unbind(logit_spans, dim=-1)
_, max_num_tokens = logit_noncat_slot_start.size()
old_logit_noncat_slot_start = logit_noncat_slot_start
logit_noncat_slot_start, noncategorical_slot_value_start = self._helper(
logit_noncat_slot_start, noncategorical_slot_value_start, task_mask[:, 5]
)
if len(noncategorical_slot_value_start) == 0:
span_start_loss = torch.clamp(torch.max(old_logit_noncat_slot_start.view(-1)), 0, 0)
else:
span_start_loss = self._cross_entropy(logit_noncat_slot_start, noncategorical_slot_value_start)
old_logit_noncat_slot_end = logit_noncat_slot_end
logit_noncat_slot_end, noncategorical_slot_value_end = self._helper(
logit_noncat_slot_end, noncategorical_slot_value_end, task_mask[:, 5]
)
if len(noncategorical_slot_value_end) == 0:
span_end_loss = torch.clamp(torch.max(old_logit_noncat_slot_end.view(-1)), 0, 0)
else:
span_end_loss = self._cross_entropy(logit_noncat_slot_end, noncategorical_slot_value_end)
losses = {
"intent_loss": intent_loss,
"requested_slot_loss": requested_slot_loss,
"cat_slot_status_loss": cat_slot_status_loss,
"cat_slot_value_status_loss": cat_slot_value_status_loss,
"noncat_slot_status_loss": noncat_slot_status_loss,
"span_start_loss": span_start_loss,
"span_end_loss": span_end_loss,
}
total_loss = sum(losses.values())
if self.reduction == 'mean':
total_loss = total_loss / len(losses)
else:
batch_size = logit_intent_status.shape[0]
total_loss = total_loss / batch_size
return total_loss
@@ -0,0 +1,16 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.metrics.classification_report import ClassificationReport
from nemo.collections.nlp.metrics.sequence_perplexity import SequencePerplexity
@@ -0,0 +1,173 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Dict, Optional
import torch
from torchmetrics import Metric
from torchmetrics.utilities.data import METRIC_EPS
__all__ = ['ClassificationReport']
class ClassificationReport(Metric):
"""
This metric computes the number of True Positive, False Negative, and False Positive examples per class.
When doing distributed training/evaluation the result of res=ClassificationReport(predictions, labels) calls
will be all-reduced between all workers using SUM operations.
If used with PytorchLightning LightningModule, include TPs, FNs, and FPs inside validation_step results.
Then aggregate them at the end of validation epoch to correctly compute validation precision, recall, f1
using get_precision_recall_f1().
Example:
def validation_step(self, batch, batch_idx):
...
tp, fn, fp, _ = self.classification_report(preds, labels)
return {'val_loss': val_loss, 'tp': tp, 'fn': fn, 'fp': fp}
def validation_epoch_end(self, outputs):
...
# calculate metrics and classification report
precision, recall, f1, report = self.classification_report.compute()
logging.info(report)
self.log('val_loss', avg_loss, prog_bar=True)
self.log('precision', precision)
self.log('f1', f1)
self.log('recall', recall)
Args:
num_classes: number of classes in the dataset
label_ids (optional): label name to label id mapping
mode: how to compute the average
dist_sync_on_step: sync across ddp
process_group: which processes to sync across
Return:
aggregated precision, recall, f1, report
"""
def __init__(
self,
num_classes: int,
label_ids: Dict[str, int] = None,
mode: str = 'macro',
dist_sync_on_step: bool = False,
process_group: Optional[Any] = None,
):
super().__init__(dist_sync_on_step=dist_sync_on_step, process_group=process_group)
self.num_classes = num_classes
if label_ids:
self.ids_to_labels = {v: k for k, v in label_ids.items()}
else:
self.ids_to_labels = None
self.mode = mode
self.add_state("tp", default=torch.zeros(num_classes), dist_reduce_fx='sum', persistent=False)
self.add_state("fn", default=torch.zeros(num_classes), dist_reduce_fx='sum', persistent=False)
self.add_state("fp", default=torch.zeros(num_classes), dist_reduce_fx='sum', persistent=False)
self.add_state(
"num_examples_per_class", default=torch.zeros(num_classes), dist_reduce_fx='sum', persistent=False
)
def update(self, predictions: torch.Tensor, labels: torch.Tensor):
TP = []
FN = []
FP = []
for label_id in range(self.num_classes):
current_label = labels == label_id
label_predicted = predictions == label_id
TP.append((label_predicted == current_label)[label_predicted].sum())
FP.append((label_predicted != current_label)[label_predicted].sum())
FN.append((label_predicted != current_label)[current_label].sum())
tp = torch.tensor(TP).to(predictions.device)
fn = torch.tensor(FN).to(predictions.device)
fp = torch.tensor(FP).to(predictions.device)
num_examples_per_class = tp + fn
self.tp += tp
self.fn += fn
self.fp += fp
self.num_examples_per_class += num_examples_per_class
def compute(self):
"""
Aggregates and then calculates logs classification report similar to sklearn.metrics.classification_report.
Typically used during epoch_end.
Return:
aggregated precision, recall, f1, report
"""
total_examples = torch.sum(self.num_examples_per_class)
num_non_empty_classes = torch.nonzero(self.num_examples_per_class).size(0)
precision = torch.true_divide(self.tp * 100, (self.tp + self.fp + METRIC_EPS))
recall = torch.true_divide(self.tp * 100, (self.tp + self.fn + METRIC_EPS))
f1 = torch.true_divide(2 * precision * recall, (precision + recall + METRIC_EPS))
report = '\n{:50s} {:10s} {:10s} {:10s} {:10s}'.format('label', 'precision', 'recall', 'f1', 'support')
for i in range(len(self.tp)):
label = f'label_id: {i}'
if self.ids_to_labels and i in self.ids_to_labels:
label = f'{self.ids_to_labels[i]} ({label})'
report += '\n{:50s} {:8.2f} {:8.2f} {:8.2f} {:8.0f}'.format(
label, precision[i], recall[i], f1[i], self.num_examples_per_class[i]
)
micro_precision = torch.true_divide(torch.sum(self.tp) * 100, torch.sum(self.tp + self.fp) + METRIC_EPS)
micro_recall = torch.true_divide(torch.sum(self.tp) * 100, torch.sum(self.tp + self.fn) + METRIC_EPS)
micro_f1 = torch.true_divide(2 * micro_precision * micro_recall, (micro_precision + micro_recall + METRIC_EPS))
macro_precision = torch.sum(precision) / num_non_empty_classes
macro_recall = torch.sum(recall) / num_non_empty_classes
macro_f1 = torch.sum(f1) / num_non_empty_classes
weighted_precision = torch.sum(precision * self.num_examples_per_class) / total_examples
weighted_recall = torch.sum(recall * self.num_examples_per_class) / total_examples
weighted_f1 = torch.sum(f1 * self.num_examples_per_class) / total_examples
report += "\n-------------------"
report += '\n{:50s} {:8.2f} {:8.2f} {:8.2f} {:8.0f}'.format(
'micro avg', micro_precision, micro_recall, micro_f1, total_examples
)
report += '\n{:50s} {:8.2f} {:8.2f} {:8.2f} {:8.0f}'.format(
'macro avg', macro_precision, macro_recall, macro_f1, total_examples
)
report += (
'\n{:50s} {:8.2f} {:8.2f} {:8.2f} {:8.0f}'.format(
'weighted avg', weighted_precision, weighted_recall, weighted_f1, total_examples
)
+ '\n'
)
self.total_examples = total_examples
if self.mode == 'macro':
return macro_precision, macro_recall, macro_f1, report
elif self.mode == 'weighted':
return weighted_precision, weighted_recall, weighted_f1, report
elif self.mode == 'micro':
return micro_precision, micro_recall, micro_f1, report
elif self.mode == 'all':
return precision, recall, f1, report
else:
raise ValueError(
f'{self.mode} mode is not supported. Choose "macro" to get aggregated numbers \
or "all" to get values for each class.'
)
@@ -0,0 +1,78 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import torch
from torchmetrics import Metric
__all__ = ['SequencePerplexity']
class SequencePerplexity(Metric):
"""
This class computes mean perplexity across the batches of sequences.
You have to provide ``log_probs`` (float tensor of shape [batch_size x seq_length x vocab_size]) and
``labels`` (int tensor of shape [batch_size x seq_length] with values from the range [0, vocab_size-1])
to the :meth:`update` method. If some of the sequences are shorter than seq_length, you can also provide
an optional argument ``mask`` (bool tensor of shape [batch_size x seq_length]) which masks out tokens
not participating in perplexity computation.
See :doc:`PyTorch Lightning Metrics<pytorch-lightning:metrics>` for the metric usage instructions.
Args:
compute_on_step:
Forward only calls ``update()`` and returns ``None`` if this is set to ``False``. default: ``True``
dist_sync_on_step:
Synchronize metric state across processes at each ``forward()`` before returning the value at the step.
process_group:
Specify the process group on which synchronization is called. default: ``None`` (which selects the entire
world)
dist_sync_fn:
Callback that performs the allgather operation on the metric state. When ``None``, DDP will be used
to perform the allgather.
"""
def __init__(self, compute_on_step=True, dist_sync_on_step=False, process_group=None, dist_sync_fn=None):
super().__init__(
compute_on_step=compute_on_step,
dist_sync_on_step=dist_sync_on_step,
process_group=process_group,
dist_sync_fn=dist_sync_fn,
)
# Total sum of exponentiated average negative log likelihoods
self.add_state('perplexities_sum', default=torch.tensor(0.0, dtype=torch.float64), dist_reduce_fx='sum')
# Total number of sequences in all batches
self.add_state('num_sequences', default=torch.tensor(0, dtype=torch.int64), dist_reduce_fx='sum')
def update(self, log_probs: torch.Tensor, labels: torch.Tensor, mask=None):
if mask is None:
mask = torch.ones_like(labels)
if mask.dtype is not log_probs.dtype:
mask = mask.to(log_probs.dtype)
target_log_probs = log_probs.gather(2, labels.unsqueeze(2)).squeeze(2)
avg_neg_ll = -(target_log_probs * mask).sum(dim=-1) / mask.sum(dim=-1)
ppl = avg_neg_ll.exp()
self.num_sequences += ppl.numel()
self.perplexities_sum += ppl.sum()
def compute(self):
"""
Returns perplexity across all workers and resets to 0 :attr:`perplexities_sum` and :attr:`num_sequences`.
"""
if self.num_sequences.eq(0):
return None
return self.perplexities_sum / self.num_sequences
@@ -0,0 +1,28 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.dialogue_state_tracking.sgdqa_model import SGDQAModel
from nemo.collections.nlp.models.entity_linking.entity_linking_model import EntityLinkingModel
from nemo.collections.nlp.models.glue_benchmark.glue_benchmark_model import GLUEModel
from nemo.collections.nlp.models.information_retrieval import BertDPRModel, BertJointIRModel
from nemo.collections.nlp.models.intent_slot_classification import IntentSlotClassificationModel
from nemo.collections.nlp.models.language_modeling.bert_lm_model import BERTLMModel
from nemo.collections.nlp.models.language_modeling.transformer_lm_model import TransformerLMModel
from nemo.collections.nlp.models.machine_translation import MTEncDecModel
from nemo.collections.nlp.models.neural_machine_translation.neural_machine_translation_model import (
NeuralMachineTranslationModel,
)
from nemo.collections.nlp.models.question_answering.qa_model import QAModel
from nemo.collections.nlp.models.text_classification import TextClassificationModel
from nemo.collections.nlp.models.token_classification import PunctuationCapitalizationModel, TokenClassificationModel
@@ -0,0 +1,15 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.dialogue_state_tracking.sgdqa_model import SGDQAModel
@@ -0,0 +1,580 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
# Copyright 2019 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
This file contains code artifacts adapted from the original implementation:
https://github.com/google-research/google-research/blob/master/schema_guided_dst/baseline/train_and_predict.py
'''
import os
from typing import Dict, List, Optional
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from torch.utils.data import DataLoader
from nemo.collections.nlp.data.dialogue_state_tracking import Schema, SGDDataProcessor, SGDDataset
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.evaluate import evaluate, get_in_domain_services
from nemo.collections.nlp.data.dialogue_state_tracking.sgd.prediction_utils import write_predictions_to_file
from nemo.collections.nlp.losses import SGDDialogueStateLoss
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules import SGDDecoder, SGDEncoder
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.parts.utils_funcs import tensor2list
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import NeuralType
from nemo.utils.get_rank import is_global_rank_zero
__all__ = ['SGDQAModel']
NUM_TASKS = 6 # number of multi-head tasks
class SGDQAModel(NLPModel):
"""Dialogue State Tracking Model SGD-QA"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return self.decoder.output_types
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
self.data_prepared = False
self.setup_tokenizer(cfg.tokenizer)
super().__init__(cfg=cfg, trainer=trainer)
self.bert_model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=OmegaConf.to_container(cfg.language_model.config) if cfg.language_model.config else None,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file),
)
self.encoder = SGDEncoder(hidden_size=self.bert_model.config.hidden_size, dropout=self._cfg.encoder.dropout)
self.decoder = SGDDecoder(embedding_dim=self.bert_model.config.hidden_size)
self.loss = SGDDialogueStateLoss(reduction="mean")
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
token_embeddings = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask
)
encoded_utterance, token_embeddings = self.encoder(hidden_states=token_embeddings)
(
logit_intent_status,
logit_req_slot_status,
logit_cat_slot_status,
logit_cat_slot_value_status,
logit_noncat_slot_status,
logit_spans,
) = self.decoder(
encoded_utterance=encoded_utterance, token_embeddings=token_embeddings, utterance_mask=attention_mask
)
return (
logit_intent_status,
logit_req_slot_status,
logit_cat_slot_status,
logit_cat_slot_value_status,
logit_noncat_slot_status,
logit_spans,
)
def training_step(self, batch, batch_idx):
(
example_id_num,
service_id,
utterance_ids,
token_type_ids,
attention_mask,
intent_status,
requested_slot_status,
categorical_slot_status,
categorical_slot_value_status,
noncategorical_slot_status,
noncategorical_slot_value_start,
noncategorical_slot_value_end,
start_char_idx,
end_char_idx,
task_mask,
) = batch
(
logit_intent_status,
logit_req_slot_status,
logit_cat_slot_status,
logit_cat_slot_value_status,
logit_noncat_slot_status,
logit_spans,
) = self(input_ids=utterance_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
loss = self.loss(
logit_intent_status=logit_intent_status,
intent_status=intent_status,
logit_req_slot_status=logit_req_slot_status,
requested_slot_status=requested_slot_status,
logit_cat_slot_status=logit_cat_slot_status,
categorical_slot_status=categorical_slot_status,
logit_cat_slot_value_status=logit_cat_slot_value_status,
categorical_slot_value_status=categorical_slot_value_status,
logit_noncat_slot_status=logit_noncat_slot_status,
noncategorical_slot_status=noncategorical_slot_status,
logit_spans=logit_spans,
noncategorical_slot_value_start=noncategorical_slot_value_start,
noncategorical_slot_value_end=noncategorical_slot_value_end,
task_mask=task_mask,
)
lr = self._optimizer.param_groups[0]['lr']
self.log('train_loss', loss)
self.log('lr', lr, prog_bar=True)
return {
'loss': loss,
'lr': lr,
}
def validation_step(self, batch: List[torch.Tensor], batch_idx: int, dataloader_idx: int = 0) -> dict:
"""
Called at every validation step to aggregate and postprocess outputs on each GPU
Args:
batch: input batch at validation step
batch_idx: batch index
dataloader_idx: dataloader index
"""
loss, tensors = self.eval_step_helper(batch=batch)
self.log(f'val_loss', loss)
return {f'val_loss': loss, f'tensors': tensors}
def test_step(self, batch: List[torch.Tensor], batch_idx: int, dataloader_idx: int = 0) -> dict:
"""
Called at every test step to aggregate and postprocess outputs on each GPU
Args:
batch: input batch at test step
batch_idx: batch index
dataloader_idx: dataloader index
"""
loss, tensors = self.eval_step_helper(batch=batch)
return {f'test_loss': loss, f'tensors': tensors}
def eval_step_helper(self, batch: List[torch.Tensor]):
"""
Helper called at every validation/test step to aggregate and postprocess outputs on each GPU
Args:
batch: input batch at step
Returns:
loss: averaged batch loss
tensors: collection of aggregated output tensors across all GPU workers
"""
(
example_id_num,
service_id,
utterance_ids,
token_type_ids,
attention_mask,
intent_status,
requested_slot_status,
categorical_slot_status,
categorical_slot_value_status,
noncategorical_slot_status,
noncategorical_slot_value_start,
noncategorical_slot_value_end,
start_char_idx,
end_char_idx,
task_mask,
) = batch
(
logit_intent_status,
logit_req_slot_status,
logit_cat_slot_status,
logit_cat_slot_value_status,
logit_noncat_slot_status,
logit_spans,
) = self(input_ids=utterance_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
loss = self.loss(
logit_intent_status=logit_intent_status,
intent_status=intent_status,
logit_req_slot_status=logit_req_slot_status,
requested_slot_status=requested_slot_status,
logit_cat_slot_status=logit_cat_slot_status,
categorical_slot_status=categorical_slot_status,
logit_cat_slot_value_status=logit_cat_slot_value_status,
categorical_slot_value_status=categorical_slot_value_status,
logit_noncat_slot_status=logit_noncat_slot_status,
noncategorical_slot_status=noncategorical_slot_status,
logit_spans=logit_spans,
noncategorical_slot_value_start=noncategorical_slot_value_start,
noncategorical_slot_value_end=noncategorical_slot_value_end,
task_mask=task_mask,
)
all_example_id_num = []
all_service_id = []
all_logit_intent_status = []
all_logit_req_slot_status = []
all_logit_cat_slot_status = []
all_logit_cat_slot_value_status = []
all_logit_noncat_slot_status = []
all_logit_spans = []
all_start_char_idx = []
all_end_char_idx = []
if self.trainer.gpus and self.trainer.world_size > 1:
world_size = self.trainer.world_size
for ind in range(world_size):
all_example_id_num.append(torch.empty_like(example_id_num))
all_service_id.append(torch.empty_like(service_id))
all_logit_intent_status.append(torch.empty_like(logit_intent_status))
all_logit_req_slot_status.append(torch.empty_like(logit_req_slot_status))
all_logit_cat_slot_status.append(torch.empty_like(logit_cat_slot_status))
all_logit_cat_slot_value_status.append(torch.empty_like(logit_cat_slot_value_status))
all_logit_noncat_slot_status.append(torch.empty_like(logit_noncat_slot_status))
all_logit_spans.append(torch.empty_like(logit_spans))
all_start_char_idx.append(torch.empty_like(start_char_idx))
all_end_char_idx.append(torch.empty_like(end_char_idx))
torch.distributed.all_gather(all_example_id_num, example_id_num)
torch.distributed.all_gather(all_service_id, service_id)
torch.distributed.all_gather(all_logit_intent_status, logit_intent_status)
torch.distributed.all_gather(all_logit_req_slot_status, logit_req_slot_status)
torch.distributed.all_gather(all_logit_cat_slot_status, logit_cat_slot_status)
torch.distributed.all_gather(all_logit_cat_slot_value_status, logit_cat_slot_value_status)
torch.distributed.all_gather(all_logit_noncat_slot_status, logit_noncat_slot_status)
torch.distributed.all_gather(all_logit_spans, logit_spans)
torch.distributed.all_gather(all_start_char_idx, start_char_idx)
torch.distributed.all_gather(all_end_char_idx, end_char_idx)
else:
all_example_id_num.append(example_id_num)
all_service_id.append(service_id)
all_logit_intent_status.append(logit_intent_status)
all_logit_req_slot_status.append(logit_req_slot_status)
all_logit_cat_slot_status.append(logit_cat_slot_status)
all_logit_cat_slot_value_status.append(logit_cat_slot_value_status)
all_logit_noncat_slot_status.append(logit_noncat_slot_status)
all_logit_spans.append(logit_spans)
all_start_char_idx.append(start_char_idx)
all_end_char_idx.append(end_char_idx)
# after this: all_x is list of tensors, of length world_size
example_id_num = torch.cat(all_example_id_num)
service_id = torch.cat(all_service_id)
logit_intent_status = torch.cat(all_logit_intent_status)
logit_req_slot_status = torch.cat(all_logit_req_slot_status)
logit_cat_slot_status = torch.cat(all_logit_cat_slot_status)
logit_cat_slot_value_status = torch.cat(all_logit_cat_slot_value_status)
logit_noncat_slot_status = torch.cat(all_logit_noncat_slot_status)
logit_spans = torch.cat(all_logit_spans)
start_char_idx = torch.cat(all_start_char_idx)
end_char_idx = torch.cat(all_end_char_idx)
intent_status = torch.nn.Sigmoid()(logit_intent_status)
# Scores are output for each requested slot.
req_slot_status = torch.nn.Sigmoid()(logit_req_slot_status)
# For categorical slots, the status of each slot and the predicted value are output.
cat_slot_status_dist = torch.nn.Softmax(dim=-1)(logit_cat_slot_status)
cat_slot_status = torch.argmax(logit_cat_slot_status, axis=-1)
cat_slot_status_p = torch.max(cat_slot_status_dist, axis=-1)[0]
cat_slot_value_status = torch.nn.Sigmoid()(logit_cat_slot_value_status)
# For non-categorical slots, the status of each slot and the indices for spans are output.
noncat_slot_status_dist = torch.nn.Softmax(dim=-1)(logit_noncat_slot_status)
noncat_slot_status = torch.argmax(logit_noncat_slot_status, axis=-1)
noncat_slot_status_p = torch.max(noncat_slot_status_dist, axis=-1)[0]
softmax = torch.nn.Softmax(dim=1)
scores = softmax(logit_spans)
start_scores, end_scores = torch.unbind(scores, dim=-1)
batch_size, max_num_tokens = end_scores.size()
# Find the span with the maximum sum of scores for start and end indices.
total_scores = torch.unsqueeze(start_scores, axis=2) + torch.unsqueeze(end_scores, axis=1)
start_idx = torch.arange(max_num_tokens, device=total_scores.get_device()).view(1, -1, 1)
end_idx = torch.arange(max_num_tokens, device=total_scores.get_device()).view(1, 1, -1)
invalid_index_mask = (start_idx > end_idx).repeat(batch_size, 1, 1)
total_scores = torch.where(
invalid_index_mask,
torch.zeros(total_scores.size(), device=total_scores.get_device(), dtype=total_scores.dtype),
total_scores,
)
max_span_index = torch.argmax(total_scores.view(-1, max_num_tokens ** 2), axis=-1)
max_span_p = torch.max(total_scores.view(-1, max_num_tokens ** 2), axis=-1)[0]
span_start_index = torch.floor_divide(max_span_index, max_num_tokens)
span_end_index = torch.fmod(max_span_index, max_num_tokens)
tensors = {
'example_id_num': example_id_num,
'service_id': service_id,
'intent_status': intent_status,
'req_slot_status': req_slot_status,
'cat_slot_status': cat_slot_status,
'cat_slot_status_p': cat_slot_status_p,
'cat_slot_value_status': cat_slot_value_status,
'noncat_slot_status': noncat_slot_status,
'noncat_slot_status_p': noncat_slot_status_p,
'noncat_slot_p': max_span_p,
'noncat_slot_start': span_start_index,
'noncat_slot_end': span_end_index,
'noncat_alignment_start': start_char_idx,
'noncat_alignment_end': end_char_idx,
}
return loss, tensors
def multi_validation_epoch_end(self, outputs: List[dict], dataloader_idx: int = 0):
"""
Called at the end of validation to post process outputs into human readable format
Args:
outputs: list of individual outputs of each validation step
dataloader_idx: dataloader index
"""
avg_loss = torch.stack([x[f'val_loss'] for x in outputs]).mean()
split = self._validation_names[dataloader_idx][:-1]
dataloader = self._validation_dl[dataloader_idx]
metrics = self.multi_eval_epoch_end_helper(outputs=outputs, split=split, dataloader=dataloader)
for k, v in metrics.items():
self.log(f'{split}_{k}', v)
self.log(f'val_loss', avg_loss, prog_bar=True)
def multi_test_epoch_end(self, outputs: List[dict], dataloader_idx: int = 0):
"""
Called at the end of test to post process outputs into human readable format
Args:
outputs: list of individual outputs of each test step
dataloader_idx: dataloader index
"""
avg_loss = torch.stack([x[f'test_loss'] for x in outputs]).mean()
split = self._test_names[dataloader_idx][:-1]
dataloader = self._test_dl[dataloader_idx]
metrics = self.multi_eval_epoch_end_helper(outputs=outputs, split=split, dataloader=dataloader)
for k, v in metrics.items():
self.log(f'{split}_{k}', v)
self.log(f'test_loss', avg_loss, prog_bar=True)
def multi_eval_epoch_end_helper(
self, outputs: List[dict], split: str, dataloader: torch.utils.data.DataLoader
) -> dict:
"""
Helper called at the end of evaluation to post process outputs into human readable format
Args:
outputs: list of individual outputs of each step
split: data split
dataloader: dataloader
Returns:
metrics: metrics collection
"""
def get_str_example_id(split: str, ids_to_service_names_dict: dict, example_id_num: torch.Tensor) -> str:
"""
Constructs string representation of example ID
Args:
split: evaluation data split
ids_to_service_names_dict: id to service name mapping
example_id_num: tensor example id
"""
def format_turn_id(ex_id_num):
dialog_id_1, dialog_id_2, turn_id, service_id, model_task_id, slot_intent_id, value_id = ex_id_num
return "{}-{}_{:05d}-{:02d}-{}-{}-{}-{}".format(
split,
dialog_id_1,
dialog_id_2,
turn_id,
ids_to_service_names_dict[service_id],
model_task_id,
slot_intent_id,
value_id,
)
return list(map(format_turn_id, tensor2list(example_id_num)))
def combine_predictions_in_example(predictions: dict, batch_size: int):
'''
Combines predicted values to a single example.
Args:
predictions: predictions ordered by keys then batch
batch_size: batch size
Returns:
examples_preds: predictions ordered by batch then key
'''
examples_preds = [{} for _ in range(batch_size)]
for k, v in predictions.items():
if k != 'example_id':
v = torch.chunk(v, batch_size)
for i in range(batch_size):
if k == 'example_id':
examples_preds[i][k] = v[i]
else:
examples_preds[i][k] = v[i].view(-1)
return examples_preds
example_id_num = torch.cat([x[f'tensors']['example_id_num'] for x in outputs])
service_id = torch.cat([x[f'tensors']['service_id'] for x in outputs])
intent_status = torch.cat([x[f'tensors']['intent_status'] for x in outputs])
req_slot_status = torch.cat([x[f'tensors']['req_slot_status'] for x in outputs])
cat_slot_status = torch.cat([x[f'tensors']['cat_slot_status'] for x in outputs])
cat_slot_status_p = torch.cat([x[f'tensors']['cat_slot_status_p'] for x in outputs])
cat_slot_value_status = torch.cat([x[f'tensors']['cat_slot_value_status'] for x in outputs])
noncat_slot_status = torch.cat([x[f'tensors']['noncat_slot_status'] for x in outputs])
noncat_slot_status_p = torch.cat([x[f'tensors']['noncat_slot_status_p'] for x in outputs])
noncat_slot_p = torch.cat([x[f'tensors']['noncat_slot_p'] for x in outputs])
noncat_slot_start = torch.cat([x[f'tensors']['noncat_slot_start'] for x in outputs])
noncat_slot_end = torch.cat([x[f'tensors']['noncat_slot_end'] for x in outputs])
noncat_alignment_start = torch.cat([x[f'tensors']['noncat_alignment_start'] for x in outputs])
noncat_alignment_end = torch.cat([x[f'tensors']['noncat_alignment_end'] for x in outputs])
ids_to_service_names_dict = self.dialogues_processor.schemas._services_id_to_vocab
example_id = get_str_example_id(dataloader.dataset, ids_to_service_names_dict, example_id_num)
metrics = {}
try:
prediction_dir = self.trainer.log_dir if self.trainer.log_dir is not None else ""
except:
prediction_dir = ""
if self.trainer.global_rank == 0:
prediction_dir = os.path.join(
prediction_dir, 'predictions', 'pred_res_{}_{}'.format(split, self._cfg.dataset.task_name)
)
os.makedirs(prediction_dir, exist_ok=True)
input_json_files = SGDDataProcessor.get_dialogue_files(
self._cfg.dataset.data_dir, split, self._cfg.dataset.task_name
)
predictions = {}
predictions['example_id'] = example_id
predictions['service_id'] = service_id
predictions['intent_status'] = intent_status
predictions['req_slot_status'] = req_slot_status
predictions['cat_slot_status'] = cat_slot_status
predictions['cat_slot_status_p'] = cat_slot_status_p
predictions['cat_slot_value_status'] = cat_slot_value_status
predictions['noncat_slot_status'] = noncat_slot_status
predictions['noncat_slot_status_p'] = noncat_slot_status_p
predictions['noncat_slot_p'] = noncat_slot_p
predictions['noncat_slot_start'] = noncat_slot_start
predictions['noncat_slot_end'] = noncat_slot_end
predictions['noncat_alignment_start'] = noncat_alignment_start
predictions['noncat_alignment_end'] = noncat_alignment_end
in_domain_services = get_in_domain_services(
os.path.join(self._cfg.dataset.data_dir, split, "schema.json"),
self.dialogues_processor.get_seen_services("train"),
)
predictions = combine_predictions_in_example(predictions, service_id.shape[0])
# write predictions to file in Dstc8/SGD format
write_predictions_to_file(
predictions,
input_json_files,
output_dir=prediction_dir,
schemas=self.dialogues_processor.schemas,
state_tracker=self._cfg.dataset.state_tracker,
eval_debug=False,
in_domain_services=in_domain_services,
)
metrics = evaluate(
prediction_dir,
self._cfg.dataset.data_dir,
split,
in_domain_services,
joint_acc_across_turn=self._cfg.dataset.joint_acc_across_turn,
use_fuzzy_match=self._cfg.dataset.use_fuzzy_match,
)
return metrics
def prepare_data(self):
"""
Preprocessed schema and dialogues and caches this
"""
if self.data_prepared:
return
schema_config = {
"MAX_NUM_CAT_SLOT": self._cfg.dataset.max_num_cat_slot,
"MAX_NUM_NONCAT_SLOT": self._cfg.dataset.max_num_noncat_slot,
"MAX_NUM_VALUE_PER_CAT_SLOT": self._cfg.dataset.max_value_per_cat_slot,
"MAX_NUM_INTENT": self._cfg.dataset.max_num_intent,
"NUM_TASKS": NUM_TASKS,
"MAX_SEQ_LENGTH": self._cfg.dataset.max_seq_length,
}
all_schema_json_paths = []
for dataset_split in ['train', 'test', 'dev']:
all_schema_json_paths.append(os.path.join(self._cfg.dataset.data_dir, dataset_split, "schema.json"))
schemas = Schema(all_schema_json_paths)
self.dialogues_processor = SGDDataProcessor(
task_name=self._cfg.dataset.task_name,
data_dir=self._cfg.dataset.data_dir,
dialogues_example_dir=self._cfg.dataset.dialogues_example_dir,
tokenizer=self.tokenizer,
schemas=schemas,
schema_config=schema_config,
subsample=self._cfg.dataset.subsample,
)
if is_global_rank_zero():
overwrite_dial_files = not self._cfg.dataset.use_cache
self.dialogues_processor.save_dialog_examples(overwrite_dial_files=overwrite_dial_files)
self.data_prepared = True
def setup_training_data(self, train_data_config: Optional[DictConfig] = None):
self.prepare_data()
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config, split=train_data_config.ds_item)
def setup_validation_data(self, val_data_config: Optional[DictConfig] = None):
self.prepare_data()
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config, split=val_data_config.ds_item)
def setup_test_data(self, test_data_config: Optional[DictConfig] = None):
self.prepare_data()
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config, split=test_data_config.ds_item)
def _setup_dataloader_from_config(self, cfg: DictConfig, split: str) -> DataLoader:
dataset_cfg = self._cfg.dataset
data_dir = dataset_cfg.data_dir
if not os.path.exists(data_dir):
raise FileNotFoundError(f"Data directory is not found at: {data_dir}.")
dataset = SGDDataset(dataset_split=split, dialogues_processor=self.dialogues_processor)
dl = torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
collate_fn=dataset.collate_fn,
drop_last=cfg.drop_last,
shuffle=cfg.shuffle,
num_workers=cfg.num_workers,
pin_memory=cfg.pin_memory,
)
return dl
@classmethod
def list_available_models(cls) -> Optional[PretrainedModelInfo]:
pass
@@ -0,0 +1,85 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from typing import Any
from omegaconf.omegaconf import MISSING
from pytorch_lightning.trainer.trainer import Trainer
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common.decoder_module import DecoderModule
from nemo.collections.nlp.modules.common.encoder_module import EncoderModule
from nemo.collections.nlp.modules.common.tokenizer_utils import TokenizerConfig
from nemo.core.config.modelPT import ModelConfig
@dataclass
class EncDecNLPModelConfig(ModelConfig):
encoder_tokenizer: TokenizerConfig = MISSING
decoder_tokenizer: TokenizerConfig = MISSING
encoder: Any = MISSING
decoder: Any = MISSING
head: Any = MISSING
class EncDecNLPModel(NLPModel):
"""Base class for encoder-decoder NLP models.
"""
def __init__(self, cfg: EncDecNLPModelConfig, trainer: Trainer = None):
super().__init__(cfg=cfg, trainer=trainer)
@property
def encoder_vocab_size(self):
return self.encoder_tokenizer.vocab_size
@property
def decoder_vocab_size(self):
return self.decoder_tokenizer.vocab_size
@property
def encoder_tokenizer(self):
return self._encoder_tokenizer
@encoder_tokenizer.setter
def encoder_tokenizer(self, tokenizer):
self._encoder_tokenizer = tokenizer
@property
def decoder_tokenizer(self):
return self._decoder_tokenizer
@decoder_tokenizer.setter
def decoder_tokenizer(self, tokenizer):
self._decoder_tokenizer = tokenizer
@property
def encoder(self) -> EncoderModule:
return self._encoder
@encoder.setter
def encoder(self, encoder):
self._encoder = encoder
@property
def decoder(self) -> DecoderModule:
return self._decoder
@decoder.setter
def decoder(self, decoder):
self._decoder = decoder
def export(self, **kwargs):
raise NotImplementedError('For EncDecNLPModel, you must export encoder and decoder separately!')
@@ -0,0 +1,15 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.entity_linking.entity_linking_model import EntityLinkingModel
@@ -0,0 +1,193 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Optional
import torch
from omegaconf import DictConfig
from pytorch_lightning import Trainer
from transformers import AutoTokenizer
from nemo.collections.common.losses import MultiSimilarityLoss
from nemo.collections.nlp.data import EntityLinkingDataset
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.core.classes.common import typecheck
from nemo.core.classes.exportable import Exportable
from nemo.core.neural_types import ChannelType, LogitsType, MaskType, NeuralType
from nemo.utils import logging
__all__ = ['EntityLinkingModel']
class EntityLinkingModel(NLPModel, Exportable):
"""
Second stage pretraining of BERT based language model
for entity linking task. An implementation of Liu et. al's
NAACL 2021 paper Self-Alignment Pretraining for Biomedical Entity Representations.
"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"input_ids": NeuralType(('B', 'T'), ChannelType()),
"attention_mask": NeuralType(('B', 'T'), MaskType(), optional=True),
"token_type_ids": NeuralType(('B', 'T'), ChannelType(), optional=True),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"logits": NeuralType(('B', 'D'), LogitsType())}
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
"""Initializes the SAP-BERT model for entity linking."""
# tokenizer needed before super().__init__() so dataset and loader can process data
self._setup_tokenizer(cfg.tokenizer)
super().__init__(cfg=cfg, trainer=trainer)
self.model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=cfg.language_model.config_file,
config_dict=cfg.language_model.config,
checkpoint_file=cfg.language_model.lm_checkpoint,
)
# Token to use for the self-alignment loss, typically the first token, [CLS]
self._idx_conditioned_on = 0
self.loss = MultiSimilarityLoss()
def _setup_tokenizer(self, cfg: DictConfig):
tokenizer = AutoTokenizer.from_pretrained(
cfg.tokenizer_name, vocab_file=cfg.vocab_file, do_lower_case=cfg.do_lower_case
)
self.tokenizer = tokenizer
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
hidden_states = self.model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
# normalize to unit sphere
logits = torch.nn.functional.normalize(hidden_states[:, self._idx_conditioned_on], p=2, dim=1)
return logits
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
input_ids, token_type_ids, attention_mask, concept_ids = batch
logits = self.forward(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
train_loss = self.loss(logits=logits, labels=concept_ids)
# No hard examples found in batch,
# shouldn't use this batch to update model weights
if train_loss == 0:
train_loss = None
lr = None
else:
lr = self._optimizer.param_groups[0]["lr"]
self.log("train_loss", train_loss)
self.log("lr", lr, prog_bar=True)
return {"loss": train_loss, "lr": lr}
def validation_step(self, batch, batch_idx):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
input_ids, input_type_ids, input_mask, concept_ids = batch
with torch.no_grad():
logits = self.forward(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
val_loss = self.loss(logits=logits, labels=concept_ids)
# No hard examples found in batch,
# val loss not used to update model weights
if val_loss == 0:
val_loss = None
else:
self.log("val_loss", val_loss)
logging.info(f"val loss: {val_loss}")
return {"val_loss": val_loss}
def validation_epoch_end(self, outputs):
"""
Called at the end of validation to aggregate outputs.
Args:
outputs: list of individual outputs of each validation step.
Returns:
"""
if outputs:
avg_loss = torch.stack([x["val_loss"] for x in outputs if x["val_loss"] != None]).mean()
self.log(f"val_loss", avg_loss, prog_bar=True)
return {"val_loss": avg_loss}
def setup_training_data(self, train_data_config: Optional[DictConfig]):
if not train_data_config or not train_data_config.data_file:
logging.info(
f"Dataloader config or file_path or processed data path for the train dataset is missing, \
so no data loader for train is created!"
)
self._train_dl = None
return
self._train_dl = self.setup_dataloader(cfg=train_data_config)
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
if not val_data_config or not val_data_config.data_file:
logging.info(
f"Dataloader config or file_path or processed data path for the val dataset is missing, \
so no data loader for validation is created!"
)
self._validation_dl = None
return
self._validation_dl = self.setup_dataloader(cfg=val_data_config)
def setup_dataloader(self, cfg: Dict, is_index_data: bool = False) -> 'torch.utils.data.DataLoader':
dataset = EntityLinkingDataset(
tokenizer=self.tokenizer,
data_file=cfg.data_file,
max_seq_length=cfg.max_seq_length,
is_index_data=is_index_data,
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
collate_fn=dataset.collate_fn,
shuffle=cfg.get("shuffle", True),
num_workers=cfg.get("num_wokers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
pass
@classmethod
def from_pretrained(cls, name: str):
pass
@@ -0,0 +1,15 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.glue_benchmark.glue_benchmark_model import GLUEModel
@@ -0,0 +1,279 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Dict, Optional, Union
import numpy as np
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from nemo.collections.common.losses import CrossEntropyLoss, MSELoss
from nemo.collections.nlp.data.glue_benchmark.glue_benchmark_dataset import GLUE_TASKS_NUM_LABELS, GLUEDataset
from nemo.collections.nlp.models.glue_benchmark.metrics_for_glue import compute_metrics
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common import SequenceClassifier, SequenceRegression
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.parts.utils_funcs import list2str, tensor2list
from nemo.core.classes import typecheck
from nemo.core.neural_types import NeuralType
from nemo.utils import logging
__all__ = ['GLUEModel']
'''
Some transformer of this code were adapted from the HuggingFace library at
https://github.com/huggingface/transformers
Example of running a pretrained BERT model on the 9 GLUE tasks, read more
about GLUE benchmark here: https://gluebenchmark.com
Download the GLUE data by running the script:
https://gist.github.com/W4ngatang/60c2bdb54d156a41194446737ce03e2e
Some of these tasks have a small dataset and training can lead to high variance
in the results between different runs. Below is the median on 5 runs
(with different seeds) for each of the metrics on the dev set of the benchmark
with an uncased BERT base model (the checkpoint bert-base-uncased)
(source https://github.com/huggingface/transformers/tree/master/examples#glue).
Task Metric Result
CoLA Matthew's corr 48.87
SST-2 Accuracy 91.74
MRPC F1/Accuracy 90.70/86.27
STS-B Person/Spearman corr. 91.39/91.04
QQP Accuracy/F1 90.79/87.66
MNLI Matched acc./Mismatched acc. 83.70/84.83
QNLI Accuracy 89.31
RTE Accuracy 71.43
WNLI Accuracy 43.66
'''
class GLUEModel(NLPModel):
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return self.pooler.output_types
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
"""
Initializes model to use BERT model for GLUE tasks.
"""
if cfg.task_name not in cfg.supported_tasks:
raise ValueError(f'{cfg.task_name} not in supported task. Choose from {cfg.supported_tasks}')
self.task_name = cfg.task_name
# needed to setup validation on multiple datasets
# MNLI task has two separate dev sets: matched and mismatched
if not self._is_model_being_restored():
if self.task_name == "mnli":
cfg.validation_ds.ds_item = [
os.path.join(cfg.dataset.data_dir, 'dev_matched.tsv'),
os.path.join(cfg.dataset.data_dir, 'dev_mismatched.tsv'),
]
else:
cfg.validation_ds.ds_item = os.path.join(cfg.dataset.data_dir, cfg.validation_ds.ds_item)
cfg.train_ds.ds_item = os.path.join(cfg.dataset.data_dir, cfg.train_ds.ds_item)
logging.info(f'Using {cfg.validation_ds.ds_item} for model evaluation.')
self.setup_tokenizer(cfg.tokenizer)
super().__init__(cfg=cfg, trainer=trainer)
num_labels = GLUE_TASKS_NUM_LABELS[self.task_name]
self.bert_model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=OmegaConf.to_container(cfg.language_model.config) if cfg.language_model.config else None,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file),
)
# uses [CLS] token for classification (the first token)
if self.task_name == "sts-b":
self.pooler = SequenceRegression(hidden_size=self.bert_model.config.hidden_size)
self.loss = MSELoss()
else:
self.pooler = SequenceClassifier(
hidden_size=self.bert_model.config.hidden_size, num_classes=num_labels, log_softmax=False
)
self.loss = CrossEntropyLoss()
def update_data_dir(self, data_dir: str) -> None:
"""
Update data directory and get data stats with Data Descriptor
Weights are later used to setup loss
Args:
data_dir: path to data directory
"""
self._cfg.dataset.data_dir = data_dir
logging.info(f'Setting model.dataset.data_dir to {data_dir}.')
if self.task_name == "mnli":
self._cfg.validation_ds.ds_item = [
os.path.join(data_dir, 'dev_matched.tsv'),
os.path.join(data_dir, 'dev_mismatched.tsv'),
]
else:
self._cfg.validation_ds.ds_item = os.path.join(data_dir, 'dev.tsv')
self._cfg.train_ds.ds_item = os.path.join(data_dir, 'train.tsv')
logging.info(f'Using {self._cfg.validation_ds.ds_item} for model evaluation.')
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask
)
output = self.pooler(hidden_states=hidden_states)
return output
def training_step(self, batch, batch_idx):
input_ids, input_type_ids, input_mask, labels = batch
model_output = self(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
if self.task_name == "sts-b":
loss = self.loss(preds=model_output, labels=labels)
else:
loss = self.loss(logits=model_output, labels=labels)
lr = self._optimizer.param_groups[0]['lr']
self.log('train_loss', loss)
self.log('lr', lr, prog_bar=True)
return {
'loss': loss,
'lr': lr,
}
def validation_step(self, batch, batch_idx, dataloader_idx=0):
input_ids, input_type_ids, input_mask, labels = batch
model_output = self(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
if self.task_name == "sts-b":
val_loss = self.loss(preds=model_output, labels=labels)
else:
val_loss = self.loss(logits=model_output, labels=labels)
if self.task_name != 'sts-b':
model_output = torch.argmax(model_output, 1)
eval_tensors = {'preds': model_output, 'labels': labels}
return {'val_loss': val_loss, 'eval_tensors': eval_tensors}
def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0):
"""
Called at the end of validation to aggregate outputs.
outputs: list of individual outputs of each validation step.
"""
avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
preds = torch.cat([x['eval_tensors']['preds'] for x in outputs])
labels = torch.cat([x['eval_tensors']['labels'] for x in outputs])
all_preds = []
all_labels = []
if torch.distributed.is_initialized():
world_size = torch.distributed.get_world_size()
for ind in range(world_size):
all_preds.append(torch.empty_like(preds))
all_labels.append(torch.empty_like(labels))
torch.distributed.all_gather(all_preds, preds)
torch.distributed.all_gather(all_labels, labels)
else:
all_preds.append(preds)
all_labels.append(labels)
tensorboard_logs = {}
if not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0:
preds = []
labels = []
for p in all_preds:
preds.extend(tensor2list(p))
for l in all_labels:
labels.extend(tensor2list(l))
tensorboard_logs = compute_metrics(self.task_name, np.array(preds), np.array(labels))
val_name = self._validation_names[dataloader_idx].upper()
logging.info(f'{val_name} evaluation: {tensorboard_logs}')
# writing labels and predictions to a file in output_dir is specified in the config
output_dir = self._cfg.output_dir
if output_dir:
os.makedirs(output_dir, exist_ok=True)
filename = os.path.join(output_dir, f'{self.task_name}_{val_name}.txt')
logging.info(f'Saving labels and predictions to {filename}')
with open(filename, 'w') as f:
f.write('labels\t' + list2str(labels) + '\n')
f.write('preds\t' + list2str(preds) + '\n')
tensorboard_logs['val_loss'] = avg_loss
for key in tensorboard_logs:
self.log(f'{key}', tensorboard_logs[key], prog_bar=True)
return {'val_loss': avg_loss, 'log': tensorboard_logs}
def setup_training_data(self, train_data_config: Optional[DictConfig] = None):
if train_data_config is None:
train_data_config = self._cfg.train_ds
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
def setup_validation_data(self, val_data_config: Optional[DictConfig] = None):
if val_data_config is None:
val_data_config = self._cfg.validation_ds
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config)
def setup_multiple_validation_data(self, val_data_config: Union[DictConfig, Dict] = None):
if val_data_config is None:
val_data_config = self._cfg.validation_ds
return super().setup_multiple_validation_data(val_data_config)
def _setup_dataloader_from_config(self, cfg: DictConfig):
file_name = cfg.ds_item
if not os.path.exists(file_name):
raise FileNotFoundError(
"GLUE datasets not found. For more details on how to get the data, see: "
"https://gist.github.com/W4ngatang/60c2bdb54d156a41194446737ce03e2e"
)
dataset = GLUEDataset(
file_name=file_name,
task_name=self.task_name,
tokenizer=self.tokenizer,
max_seq_length=self._cfg.dataset.max_seq_length,
use_cache=self._cfg.dataset.use_cache,
)
return torch.utils.data.DataLoader(
dataset=dataset,
collate_fn=dataset.collate_fn,
batch_size=cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=self._cfg.dataset.num_workers,
pin_memory=self._cfg.dataset.pin_memory,
drop_last=self._cfg.dataset.drop_last,
)
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
pass
@@ -0,0 +1,66 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, List
from scipy.stats import pearsonr, spearmanr
from sklearn.metrics import f1_score, matthews_corrcoef
__all__ = ['compute_metrics']
def accuracy(preds: List[int], labels: List[int]):
return {"acc": (preds == labels).mean()}
def acc_and_f1(preds: List[int], labels: List[int]):
accuracy = (preds == labels).mean()
f1 = f1_score(y_true=labels, y_pred=preds)
return {"acc": accuracy, "f1": f1}
def mcc(preds: List[int], labels: List[int]):
return {"mcc": matthews_corrcoef(labels, preds)}
def pearson_and_spearman(preds: List[int], labels: List[int]):
pearson_corr = pearsonr(preds, labels)[0]
spearman_corr = spearmanr(preds, labels)[0]
return {"pearson": pearson_corr, "spearmanr": spearman_corr, "pear+spear av": (pearson_corr + spearman_corr) / 2}
def compute_metrics(task_name: str, preds: List[int], labels: List[int]) -> Dict[str, float]:
"""
Computes metrics for GLUE tasks
Args:
task_name: GLUE task name
preds: model predictions
labels: golden labels
Returns:
metrics
"""
if len(preds) != len(labels):
raise ValueError("Predictions and labels must have the same length")
metric_fn = accuracy
if task_name == 'cola':
metric_fn = mcc
elif task_name in ['mrpc', 'qqp']:
metric_fn = acc_and_f1
elif task_name == 'sts-b':
metric_fn = pearson_and_spearman
return metric_fn(preds, labels)
@@ -0,0 +1,16 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.information_retrieval.bert_dpr_model import BertDPRModel
from nemo.collections.nlp.models.information_retrieval.bert_joint_ir_model import BertJointIRModel
@@ -0,0 +1,211 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
from typing import Dict, Optional
import numpy as np
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from nemo.collections.nlp.data import BertInformationRetrievalDataset
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.core.classes.common import typecheck
__all__ = ['BaseIRModel']
class BaseIRModel(NLPModel):
"""
Base class for information retrieval models.
"""
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
self.setup_tokenizer(cfg.tokenizer)
super().__init__(cfg=cfg, trainer=trainer)
@typecheck()
def forward(self, *args):
pass
def compute_scores_and_loss(self, inputs):
pass
@staticmethod
def get_lm_model_with_padded_embedding(cfg: DictConfig):
"""
Function which ensures that vocabulary size is divisivble by 8
for faster mixed precision training.
"""
model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=cfg.language_model.config_file,
config_dict=OmegaConf.to_container(cfg.language_model.config) if cfg.language_model.config else None,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=cfg.tokenizer.vocab_file,
)
vocab_size, hidden_size = model.config.vocab_size, model.config.hidden_size
tokens_to_add = 8 * math.ceil(vocab_size / 8) - vocab_size
zeros = torch.zeros((tokens_to_add, hidden_size))
model.embeddings.word_embeddings.weight.data = torch.cat((model.embeddings.word_embeddings.weight.data, zeros))
return model
@staticmethod
def calculate_mean_reciprocal_rank(query2passages, query2rel):
"""
Helper function which calculates mean reciprocal rank.
Args:
query2passages: dict which contains passage ids and corresponding
scores for each query
query2rel: dict which contains ids of relevant passages for each query
"""
reciprocal_ranks = []
for query in query2passages:
indices = np.argsort(query2passages[query]["scores"])[::-1]
sorted_psgs = query2passages[query]["psg_ids"][indices]
reciprocal_ranks.append(0)
for i, psg_id in enumerate(sorted_psgs):
if psg_id in query2rel[query]:
reciprocal_ranks[-1] = 1 / (i + 1)
break
return np.mean(reciprocal_ranks)
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
scores, train_loss = self.compute_scores_and_loss(batch[:-2])
tensorboard_logs = {"train_loss": train_loss, "lr": self._optimizer.param_groups[0]["lr"]}
return {"loss": train_loss, "log": tensorboard_logs}
def validation_step(self, batch, batch_idx):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
scores, val_loss = self.compute_scores_and_loss(batch[:-2])
query_ids, passage_ids = batch[-2:]
data_for_val = {
"val_loss": val_loss,
"scores": scores,
"query_ids": query_ids,
"passage_ids": passage_ids,
}
return data_for_val
def validation_epoch_end(self, outputs):
"""
Called at the end of validation to aggregate outputs.
:param outputs: list of individual outputs of each validation step.
"""
query_ids = torch.cat([x["query_ids"] for x in outputs])
passage_ids = torch.cat([x["passage_ids"] for x in outputs])
scores = torch.cat([x["scores"] for x in outputs])
all_query_ids, all_passage_ids, all_scores = [], [], []
if torch.distributed.is_initialized():
world_size = torch.distributed.get_world_size()
for ind in range(world_size):
all_query_ids.append(torch.empty_like(query_ids))
all_passage_ids.append(torch.empty_like(passage_ids))
all_scores.append(torch.empty_like(scores))
torch.distributed.all_gather(all_query_ids, query_ids)
torch.distributed.all_gather(all_passage_ids, passage_ids)
torch.distributed.all_gather(all_scores, scores)
else:
all_query_ids.append(query_ids)
all_passage_ids.append(passage_ids)
all_scores.append(scores)
val_mrr = 0
if not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0:
query2passages, query2rels = {}, {}
processed_queries = set()
for i in range(len(all_query_ids)):
query_ids = all_query_ids[i].detach().cpu().numpy()
passage_ids = all_passage_ids[i].detach().cpu().numpy()
scores = all_scores[i].detach().cpu().numpy()
for j, query_id in enumerate(query_ids):
if query_id not in processed_queries:
processed_queries.add(query_id)
query2passages[query_id] = {
"psg_ids": passage_ids[j],
"scores": scores[j],
}
query2rels[query_id] = [passage_ids[j][0]]
else:
query2passages[query_id]["psg_ids"] = np.concatenate(
(query2passages[query_id]["psg_ids"], passage_ids[j][1:])
)
query2passages[query_id]["scores"] = np.concatenate(
(query2passages[query_id]["scores"], scores[j][1:])
)
val_mrr = self.calculate_mean_reciprocal_rank(query2passages, query2rels)
val_loss = torch.stack([x["val_loss"] for x in outputs]).mean()
tensorboard_logs = {
"val_mrr": val_mrr,
"val_loss": val_loss,
}
return {"log": tensorboard_logs}
def setup_training_data(self, train_data_config: Optional[DictConfig]):
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config)
def _setup_dataloader_from_config(self, cfg: DictConfig):
dataset = BertInformationRetrievalDataset(
tokenizer=self.tokenizer,
passages=cfg.passages,
queries=cfg.queries,
query_to_passages=cfg.query_to_passages,
num_negatives=cfg.num_negatives,
psg_cache_format=cfg.get("psg_cache_format", "pkl"),
max_query_length=cfg.get("max_query_length", 31),
max_passage_length=cfg.get("max_passage_length", 190),
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
pass
@@ -0,0 +1,131 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Optional
import torch
from omegaconf import DictConfig
from pytorch_lightning import Trainer
from nemo.collections.common.losses import SmoothedCrossEntropyLoss
from nemo.collections.nlp.data import BertInformationRetrievalDataset
from nemo.collections.nlp.models.information_retrieval.base_ir_model import BaseIRModel
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer
from nemo.core.classes.common import typecheck
from nemo.core.neural_types import ChannelType, LogitsType, MaskType, NeuralType
__all__ = ["BertDPRModel"]
class BertDPRModel(BaseIRModel):
"""
Information retrieval model which encodes query and passage separately
with two different BERT encoders and computes their similarity score
as a dot-product between corresponding [CLS] token representations.
"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"q_input_ids": NeuralType(("B", "T"), ChannelType()),
"q_attention_mask": NeuralType(("B", "T"), MaskType()),
"q_token_type_ids": NeuralType(("B", "T"), ChannelType()),
"p_input_ids": NeuralType(("B", "T"), ChannelType()),
"p_attention_mask": NeuralType(("B", "T"), MaskType()),
"p_token_type_ids": NeuralType(("B", "T"), ChannelType()),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"logits": NeuralType(("B", "D"), LogitsType())}
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
model_name = cfg.language_model.pretrained_model_name
self.tokenizer = get_tokenizer(tokenizer_name=model_name)
super().__init__(cfg=cfg, trainer=trainer)
self.q_encoder = self.get_lm_model_with_padded_embedding(cfg)
self.p_encoder = self.get_lm_model_with_padded_embedding(cfg)
self.loss = SmoothedCrossEntropyLoss(pad_id=self.tokenizer.pad_id)
@typecheck()
def forward(
self, q_input_ids, q_token_type_ids, q_attention_mask, p_input_ids, p_token_type_ids, p_attention_mask,
):
q_vectors = self.q_encoder(
input_ids=q_input_ids, token_type_ids=q_token_type_ids, attention_mask=q_attention_mask,
)
q_vectors = q_vectors[:, 0]
batch_size, hidden_size = q_vectors.size()
p_vectors = self.p_encoder(
input_ids=p_input_ids, token_type_ids=p_token_type_ids, attention_mask=p_attention_mask,
)
num_passages = p_vectors.shape[0] // batch_size
p_vectors = p_vectors[:, 0].view(-1, num_passages, hidden_size)
p_positives, p_negatives = p_vectors[:, 0], p_vectors[:, 1:]
scores = torch.cat(
(torch.matmul(q_vectors, p_positives.T), torch.einsum("ij,ipj->ip", q_vectors, p_negatives),), dim=1,
)
return scores
def compute_scores_and_loss(self, inputs):
(q_input_ids, q_input_mask, q_input_type_ids, p_input_ids, p_input_mask, p_input_type_ids,) = inputs
batch_size, num_passages, p_seq_length = p_input_ids.size()
q_seq_length = q_input_ids.size()[-1]
scores = self(
q_input_ids=q_input_ids.view(-1, q_seq_length),
q_token_type_ids=q_input_type_ids.view(-1, q_seq_length),
q_attention_mask=q_input_mask.view(-1, q_seq_length),
p_input_ids=p_input_ids.view(-1, p_seq_length),
p_token_type_ids=p_input_type_ids.view(-1, p_seq_length),
p_attention_mask=p_input_mask.view(-1, p_seq_length),
).view(batch_size, 1, batch_size + num_passages - 1)
normalized_scores = torch.log_softmax(scores, dim=-1)
labels = torch.arange(batch_size)[:, None].long().to(normalized_scores.device)
loss = self.loss(log_probs=normalized_scores, labels=labels, output_mask=torch.ones_like(labels),)
scores = scores[:, 0]
scores = torch.cat((torch.diag(scores)[:, None], scores[:, batch_size:]), dim=1,)
return scores, loss
def _setup_dataloader_from_config(self, cfg: DictConfig):
dataset = BertInformationRetrievalDataset(
tokenizer=self.tokenizer,
passages=cfg.passages,
queries=cfg.queries,
query_to_passages=cfg.query_to_passages,
num_negatives=cfg.num_negatives,
psg_cache_format=cfg.get("psg_cache_format", "pkl"),
max_query_length=cfg.get("max_query_length", 31),
max_passage_length=cfg.get("max_passage_length", 190),
preprocess_fn="preprocess_dpr",
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
@@ -0,0 +1,84 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Optional
import torch
from omegaconf import DictConfig
from pytorch_lightning import Trainer
from nemo.collections.common.losses import SmoothedCrossEntropyLoss
from nemo.collections.nlp.models.information_retrieval.base_ir_model import BaseIRModel
from nemo.collections.nlp.modules.common import SequenceRegression
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer
from nemo.core.classes.common import typecheck
from nemo.core.neural_types import NeuralType
__all__ = ["BertJointIRModel"]
class BertJointIRModel(BaseIRModel):
"""
Information retrieval model which jointly encodes both query and passage
and passes them to BERT encoder followed by a fully-connected layer for
similarity score prediction.
"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return self.sim_score_regressor.output_types
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
model_name = cfg.language_model.pretrained_model_name
self.tokenizer = get_tokenizer(tokenizer_name=model_name)
super().__init__(cfg=cfg, trainer=trainer)
self.bert_model = self.get_lm_model_with_padded_embedding(cfg)
hidden_size = self.bert_model.config.hidden_size
self.sim_score_regressor = SequenceRegression(
hidden_size=hidden_size, num_layers=1, dropout=cfg.language_model.sim_score_dropout,
)
self.loss = SmoothedCrossEntropyLoss(pad_id=self.tokenizer.pad_id)
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask,
)
scores = self.sim_score_regressor(hidden_states=hidden_states)
return scores
def compute_scores_and_loss(self, inputs):
input_ids, input_mask, input_type_ids = inputs
batch_size, num_passages, seq_length = input_ids.size()
unnormalized_scores = self(
input_ids=input_ids.view(-1, seq_length),
token_type_ids=input_type_ids.view(-1, seq_length),
attention_mask=input_mask.view(-1, seq_length),
).view(batch_size, 1, num_passages)
scores = torch.log_softmax(unnormalized_scores, dim=-1)
labels = torch.zeros_like(input_ids[:, :1, 0])
loss = self.loss(log_probs=scores, labels=labels, output_mask=torch.ones_like(labels))
return unnormalized_scores[:, 0], loss
@@ -0,0 +1,17 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.intent_slot_classification.intent_slot_classification_model import (
IntentSlotClassificationModel,
)
@@ -0,0 +1,487 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Dict, List, Optional
import onnx
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from torch.utils.data import DataLoader
from nemo.collections.common.losses import AggregatorLoss, CrossEntropyLoss
from nemo.collections.nlp.data.intent_slot_classification import (
IntentSlotClassificationDataset,
IntentSlotDataDesc,
IntentSlotInferenceDataset,
)
from nemo.collections.nlp.metrics.classification_report import ClassificationReport
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common import SequenceTokenClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.parts.utils_funcs import tensor2list
from nemo.core.classes import typecheck
from nemo.core.classes.common import PretrainedModelInfo
from nemo.core.neural_types import NeuralType
from nemo.utils import logging
class IntentSlotClassificationModel(NLPModel):
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return self.classifier.output_types
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
""" Initializes BERT Joint Intent and Slot model.
"""
self.max_seq_length = cfg.language_model.max_seq_length
# Setup tokenizer.
self.setup_tokenizer(cfg.tokenizer)
# Check the presence of data_dir.
if not cfg.data_dir or not os.path.exists(cfg.data_dir):
# Disable setup methods.
IntentSlotClassificationModel._set_model_restore_state(is_being_restored=True)
# Set default values of data_desc.
self._set_defaults_data_desc(cfg)
else:
self.data_dir = cfg.data_dir
# Update configuration of data_desc.
self._set_data_desc_to_cfg(cfg, cfg.data_dir, cfg.train_ds, cfg.validation_ds)
# init superclass
super().__init__(cfg=cfg, trainer=trainer)
# Enable setup methods.
IntentSlotClassificationModel._set_model_restore_state(is_being_restored=False)
# Initialize Bert model
self.bert_model = get_lm_model(
pretrained_model_name=self.cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=OmegaConf.to_container(self.cfg.language_model.config)
if self.cfg.language_model.config
else None,
checkpoint_file=self.cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file),
)
# Initialize Classifier.
self._reconfigure_classifier()
def _set_defaults_data_desc(self, cfg):
"""
Method makes sure that cfg.data_desc params are set.
If not, set's them to "dummy" defaults.
"""
if not hasattr(cfg, "data_desc"):
OmegaConf.set_struct(cfg, False)
cfg.data_desc = {}
# Intents.
cfg.data_desc.intent_labels = " "
cfg.data_desc.intent_label_ids = {" ": 0}
cfg.data_desc.intent_weights = [1]
# Slots.
cfg.data_desc.slot_labels = " "
cfg.data_desc.slot_label_ids = {" ": 0}
cfg.data_desc.slot_weights = [1]
cfg.data_desc.pad_label = "O"
OmegaConf.set_struct(cfg, True)
def _set_data_desc_to_cfg(self, cfg, data_dir, train_ds, validation_ds):
""" Method creates IntentSlotDataDesc and copies generated values to cfg.data_desc. """
# Save data from data desc to config - so it can be reused later, e.g. in inference.
data_desc = IntentSlotDataDesc(data_dir=data_dir, modes=[train_ds.prefix, validation_ds.prefix])
OmegaConf.set_struct(cfg, False)
if not hasattr(cfg, "data_desc") or cfg.data_desc is None:
cfg.data_desc = {}
# Intents.
cfg.data_desc.intent_labels = list(data_desc.intents_label_ids.keys())
cfg.data_desc.intent_label_ids = data_desc.intents_label_ids
cfg.data_desc.intent_weights = data_desc.intent_weights
# Slots.
cfg.data_desc.slot_labels = list(data_desc.slots_label_ids.keys())
cfg.data_desc.slot_label_ids = data_desc.slots_label_ids
cfg.data_desc.slot_weights = data_desc.slot_weights
cfg.data_desc.pad_label = data_desc.pad_label
# for older(pre - 1.0.0.b3) configs compatibility
if not hasattr(cfg, "class_labels") or cfg.class_labels is None:
cfg.class_labels = {}
cfg.class_labels = OmegaConf.create(
{'intent_labels_file': 'intent_labels.csv', 'slot_labels_file': 'slot_labels.csv'}
)
slot_labels_file = os.path.join(data_dir, cfg.class_labels.slot_labels_file)
intent_labels_file = os.path.join(data_dir, cfg.class_labels.intent_labels_file)
self._save_label_ids(data_desc.slots_label_ids, slot_labels_file)
self._save_label_ids(data_desc.intents_label_ids, intent_labels_file)
self.register_artifact('class_labels.intent_labels_file', intent_labels_file)
self.register_artifact('class_labels.slot_labels_file', slot_labels_file)
OmegaConf.set_struct(cfg, True)
def _save_label_ids(self, label_ids: Dict[str, int], filename: str) -> None:
""" Saves label ids map to a file """
with open(filename, 'w') as out:
labels, _ = zip(*sorted(label_ids.items(), key=lambda x: x[1]))
out.write('\n'.join(labels))
logging.info(f'Labels: {label_ids}')
logging.info(f'Labels mapping saved to : {out.name}')
def _reconfigure_classifier(self):
""" Method reconfigures the classifier depending on the settings of model cfg.data_desc """
self.classifier = SequenceTokenClassifier(
hidden_size=self.bert_model.config.hidden_size,
num_intents=len(self.cfg.data_desc.intent_labels),
num_slots=len(self.cfg.data_desc.slot_labels),
dropout=self.cfg.head.fc_dropout,
num_layers=self.cfg.head.num_output_layers,
log_softmax=False,
)
# define losses
if self.cfg.class_balancing == 'weighted_loss':
# You may need to increase the number of epochs for convergence when using weighted_loss
self.intent_loss = CrossEntropyLoss(logits_ndim=2, weight=self.cfg.data_desc.intent_weights)
self.slot_loss = CrossEntropyLoss(logits_ndim=3, weight=self.cfg.data_desc.slot_weights)
else:
self.intent_loss = CrossEntropyLoss(logits_ndim=2)
self.slot_loss = CrossEntropyLoss(logits_ndim=3)
self.total_loss = AggregatorLoss(
num_inputs=2, weights=[self.cfg.intent_loss_weight, 1.0 - self.cfg.intent_loss_weight]
)
# setup to track metrics
self.intent_classification_report = ClassificationReport(
num_classes=len(self.cfg.data_desc.intent_labels),
label_ids=self.cfg.data_desc.intent_label_ids,
dist_sync_on_step=True,
mode='micro',
)
self.slot_classification_report = ClassificationReport(
num_classes=len(self.cfg.data_desc.slot_labels),
label_ids=self.cfg.data_desc.slot_label_ids,
dist_sync_on_step=True,
mode='micro',
)
def update_data_dir_for_training(self, data_dir: str, train_ds, validation_ds) -> None:
"""
Update data directory and get data stats with Data Descriptor.
Also, reconfigures the classifier - to cope with data with e.g. different number of slots.
Args:
data_dir: path to data directory
"""
logging.info(f'Setting data_dir to {data_dir}.')
self.data_dir = data_dir
# Update configuration with new data.
self._set_data_desc_to_cfg(self.cfg, data_dir, train_ds, validation_ds)
# Reconfigure the classifier for different settings (number of intents, slots etc.).
self._reconfigure_classifier()
def update_data_dir_for_testing(self, data_dir) -> None:
"""
Update data directory.
Args:
data_dir: path to data directory
"""
logging.info(f'Setting data_dir to {data_dir}.')
self.data_dir = data_dir
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
"""
No special modification required for Lightning, define it as you normally would
in the `nn.Module` in vanilla PyTorch.
"""
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask
)
intent_logits, slot_logits = self.classifier(hidden_states=hidden_states)
return intent_logits, slot_logits
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
# forward pass
input_ids, input_type_ids, input_mask, loss_mask, subtokens_mask, intent_labels, slot_labels = batch
intent_logits, slot_logits = self(
input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask
)
# calculate combined loss for intents and slots
intent_loss = self.intent_loss(logits=intent_logits, labels=intent_labels)
slot_loss = self.slot_loss(logits=slot_logits, labels=slot_labels, loss_mask=loss_mask)
train_loss = self.total_loss(loss_1=intent_loss, loss_2=slot_loss)
lr = self._optimizer.param_groups[0]['lr']
self.log('train_loss', train_loss)
self.log('lr', lr, prog_bar=True)
return {
'loss': train_loss,
'lr': lr,
}
def validation_step(self, batch, batch_idx):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
input_ids, input_type_ids, input_mask, loss_mask, subtokens_mask, intent_labels, slot_labels = batch
intent_logits, slot_logits = self(
input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask
)
# calculate combined loss for intents and slots
intent_loss = self.intent_loss(logits=intent_logits, labels=intent_labels)
slot_loss = self.slot_loss(logits=slot_logits, labels=slot_labels, loss_mask=loss_mask)
val_loss = self.total_loss(loss_1=intent_loss, loss_2=slot_loss)
# calculate accuracy metrics for intents and slot reporting
# intents
preds = torch.argmax(intent_logits, axis=-1)
self.intent_classification_report.update(preds, intent_labels)
# slots
subtokens_mask = subtokens_mask > 0.5
preds = torch.argmax(slot_logits, axis=-1)[subtokens_mask]
slot_labels = slot_labels[subtokens_mask]
self.slot_classification_report.update(preds, slot_labels)
return {
'val_loss': val_loss,
'intent_tp': self.intent_classification_report.tp,
'intent_fn': self.intent_classification_report.fn,
'intent_fp': self.intent_classification_report.fp,
'slot_tp': self.slot_classification_report.tp,
'slot_fn': self.slot_classification_report.fn,
'slot_fp': self.slot_classification_report.fp,
}
def validation_epoch_end(self, outputs):
"""
Called at the end of validation to aggregate outputs.
:param outputs: list of individual outputs of each validation step.
"""
avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
# calculate metrics and log classification report (separately for intents and slots)
intent_precision, intent_recall, intent_f1, intent_report = self.intent_classification_report.compute()
logging.info(f'Intent report: {intent_report}')
slot_precision, slot_recall, slot_f1, slot_report = self.slot_classification_report.compute()
logging.info(f'Slot report: {slot_report}')
self.log('val_loss', avg_loss)
self.log('intent_precision', intent_precision)
self.log('intent_recall', intent_recall)
self.log('intent_f1', intent_f1)
self.log('slot_precision', slot_precision)
self.log('slot_recall', slot_recall)
self.log('slot_f1', slot_f1)
self.intent_classification_report.reset()
self.slot_classification_report.reset()
return {
'val_loss': avg_loss,
'intent_precision': intent_precision,
'intent_recall': intent_recall,
'intent_f1': intent_f1,
'slot_precision': slot_precision,
'slot_recall': slot_recall,
'slot_f1': slot_f1,
}
def test_step(self, batch, batch_idx):
"""
Lightning calls this inside the test loop with the data from the test dataloader
passed in as `batch`.
"""
return self.validation_step(batch, batch_idx)
def test_epoch_end(self, outputs):
"""
Called at the end of test to aggregate outputs.
:param outputs: list of individual outputs of each test step.
"""
return self.validation_epoch_end(outputs)
def setup_training_data(self, train_data_config: Optional[DictConfig]):
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config)
def _setup_dataloader_from_config(self, cfg: DictConfig):
input_file = f'{self.data_dir}/{cfg.prefix}.tsv'
slot_file = f'{self.data_dir}/{cfg.prefix}_slots.tsv'
if not (os.path.exists(input_file) and os.path.exists(slot_file)):
raise FileNotFoundError(
f'{input_file} or {slot_file} not found. Please refer to the documentation for the right format \
of Intents and Slots files.'
)
dataset = IntentSlotClassificationDataset(
input_file=input_file,
slot_file=slot_file,
tokenizer=self.tokenizer,
max_seq_length=self.max_seq_length,
num_samples=cfg.num_samples,
pad_label=self.cfg.data_desc.pad_label,
ignore_extra_tokens=self.cfg.ignore_extra_tokens,
ignore_start_end=self.cfg.ignore_start_end,
)
return DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=cfg.num_workers,
pin_memory=cfg.pin_memory,
drop_last=cfg.drop_last,
collate_fn=dataset.collate_fn,
)
def _setup_infer_dataloader(self, queries: List[str], test_ds) -> 'torch.utils.data.DataLoader':
"""
Setup function for a infer data loader.
Args:
queries: text
batch_size: batch size to use during inference
Returns:
A pytorch DataLoader.
"""
dataset = IntentSlotInferenceDataset(
tokenizer=self.tokenizer, queries=queries, max_seq_length=-1, do_lower_case=False
)
return torch.utils.data.DataLoader(
dataset=dataset,
collate_fn=dataset.collate_fn,
batch_size=test_ds.batch_size,
shuffle=test_ds.shuffle,
num_workers=test_ds.num_workers,
pin_memory=test_ds.pin_memory,
drop_last=test_ds.drop_last,
)
def predict_from_examples(self, queries: List[str], test_ds) -> List[List[str]]:
"""
Get prediction for the queries (intent and slots)
Args:
queries: text sequences
test_ds: Dataset configuration section.
Returns:
predicted_intents, predicted_slots: model intent and slot predictions
"""
predicted_intents = []
predicted_slots = []
mode = self.training
try:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Retrieve intent and slot vocabularies from configuration.
intent_labels = self.cfg.data_desc.intent_labels
slot_labels = self.cfg.data_desc.slot_labels
# Initialize tokenizer.
# if not hasattr(self, "tokenizer"):
# self._setup_tokenizer(self.cfg.tokenizer)
# Initialize modules.
# self._reconfigure_classifier()
# Switch model to evaluation mode
self.eval()
self.to(device)
# Dataset.
infer_datalayer = self._setup_infer_dataloader(queries, test_ds)
for batch in infer_datalayer:
input_ids, input_type_ids, input_mask, loss_mask, subtokens_mask = batch
intent_logits, slot_logits = self.forward(
input_ids=input_ids.to(device),
token_type_ids=input_type_ids.to(device),
attention_mask=input_mask.to(device),
)
# predict intents and slots for these examples
# intents
intent_preds = tensor2list(torch.argmax(intent_logits, axis=-1))
# convert numerical outputs to Intent and Slot labels from the dictionaries
for intent_num in intent_preds:
if intent_num < len(intent_labels):
predicted_intents.append(intent_labels[int(intent_num)])
else:
# should not happen
predicted_intents.append("Unknown Intent")
# slots
slot_preds = torch.argmax(slot_logits, axis=-1)
for slot_preds_query, mask_query in zip(slot_preds, subtokens_mask):
query_slots = ''
for slot, mask in zip(slot_preds_query, mask_query):
if mask == 1:
if slot < len(slot_labels):
query_slots += slot_labels[int(slot)] + ' '
else:
query_slots += 'Unknown_slot '
predicted_slots.append(query_slots.strip())
finally:
# set mode back to its original value
self.train(mode=mode)
return predicted_intents, predicted_slots
@classmethod
def list_available_models(cls) -> Optional[PretrainedModelInfo]:
"""
This method returns a list of pre-trained model which can be instantiated directly from NVIDIA's NGC cloud.
Returns:
List of available pre-trained models.
"""
result = []
model = PretrainedModelInfo(
pretrained_model_name="Joint_Intent_Slot_Assistant",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemonlpmodels/versions/1.0.0a5/files/Joint_Intent_Slot_Assistant.nemo",
description="This models is trained on this https://github.com/xliuhw/NLU-Evaluation-Data dataset which includes 64 various intents and 55 slots. Final Intent accuracy is about 87%, Slot accuracy is about 89%.",
)
result.append(model)
return result
@@ -0,0 +1,16 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.language_modeling.bert_lm_model import BERTLMModel
from nemo.collections.nlp.models.language_modeling.transformer_lm_model import TransformerLMModel
@@ -0,0 +1,272 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Dict, Optional
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from torch.utils.data import DataLoader
from nemo.collections.common.losses import AggregatorLoss, CrossEntropyLoss, SmoothedCrossEntropyLoss
from nemo.collections.common.metrics import Perplexity
from nemo.collections.nlp.data.language_modeling.lm_bert_dataset import (
BertPretrainingDataset,
BertPretrainingPreprocessedDataloader,
)
from nemo.collections.nlp.modules.common import BertPretrainingTokenClassifier, SequenceClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.classes.modelPT import ModelPT
from nemo.core.neural_types import NeuralType
from nemo.utils import logging
__all__ = ["BERTLMModel"]
class BERTLMModel(ModelPT):
"""
BERT language model pretraining.
"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
output_types_dict = {"mlm_log_probs": self.mlm_classifier.output_types["log_probs"]}
if not self.only_mlm_loss:
output_types_dict["nsp_logits"] = self.nsp_classifier.output_types["logits"]
return output_types_dict
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
if cfg.tokenizer is not None:
self._setup_tokenizer(cfg.tokenizer)
else:
self.tokenizer = None
super().__init__(cfg=cfg, trainer=trainer)
self.bert_model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=OmegaConf.to_container(cfg.language_model.config) if cfg.language_model.config else None,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file)
if cfg.tokenizer
else None,
)
self.hidden_size = self.bert_model.config.hidden_size
self.vocab_size = self.bert_model.config.vocab_size
self.only_mlm_loss = cfg.only_mlm_loss
self.mlm_classifier = BertPretrainingTokenClassifier(
hidden_size=self.hidden_size,
num_classes=self.vocab_size,
num_layers=cfg.num_tok_classification_layers,
activation="gelu",
log_softmax=True,
use_transformer_init=True,
)
self.mlm_loss = SmoothedCrossEntropyLoss()
if not self.only_mlm_loss:
self.nsp_classifier = SequenceClassifier(
hidden_size=self.hidden_size,
num_classes=2,
num_layers=cfg.num_seq_classification_layers,
log_softmax=False,
activation="tanh",
use_transformer_init=True,
)
self.nsp_loss = CrossEntropyLoss()
self.agg_loss = AggregatorLoss(num_inputs=2)
# # tie weights of MLM softmax layer and embedding layer of the encoder
if (
self.mlm_classifier.mlp.last_linear_layer.weight.shape
!= self.bert_model.embeddings.word_embeddings.weight.shape
):
raise ValueError("Final classification layer does not match embedding layer.")
self.mlm_classifier.mlp.last_linear_layer.weight = self.bert_model.embeddings.word_embeddings.weight
# create extra bias
# setup to track metrics
self.validation_perplexity = Perplexity(compute_on_step=False)
self.setup_optimization(cfg.optim)
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
"""
No special modification required for Lightning, define it as you normally would
in the `nn.Module` in vanilla PyTorch.
"""
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask,
)
mlm_log_probs = self.mlm_classifier(hidden_states=hidden_states)
if self.only_mlm_loss:
return (mlm_log_probs,)
nsp_logits = self.nsp_classifier(hidden_states=hidden_states)
return mlm_log_probs, nsp_logits
def _compute_losses(self, mlm_log_probs, nsp_logits, output_ids, output_mask, labels):
mlm_loss = self.mlm_loss(log_probs=mlm_log_probs, labels=output_ids, output_mask=output_mask)
if self.only_mlm_loss:
loss, nsp_loss = mlm_loss, None
else:
nsp_loss = self.nsp_loss(logits=nsp_logits, labels=labels)
loss = self.agg_loss(loss_1=mlm_loss, loss_2=nsp_loss)
return mlm_loss, nsp_loss, loss
def _parse_forward_outputs(self, forward_outputs):
if self.only_mlm_loss:
return forward_outputs[0], None
else:
return forward_outputs
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
input_ids, input_type_ids, input_mask, output_ids, output_mask, labels = batch
forward_outputs = self.forward(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
mlm_log_probs, nsp_logits = self._parse_forward_outputs(forward_outputs)
_, _, loss = self._compute_losses(mlm_log_probs, nsp_logits, output_ids, output_mask, labels)
lr = self._optimizer.param_groups[0]['lr']
self.log('train_loss', loss)
self.log('lr', lr, prog_bar=True)
return {"loss": loss, "lr": lr}
def validation_step(self, batch, batch_idx):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
input_ids, input_type_ids, input_mask, output_ids, output_mask, labels = batch
forward_outputs = self.forward(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
mlm_log_probs, nsp_logits = self._parse_forward_outputs(forward_outputs)
_, _, loss = self._compute_losses(mlm_log_probs, nsp_logits, output_ids, output_mask, labels)
self.validation_perplexity(logits=mlm_log_probs)
return {'val_loss': loss}
def validation_epoch_end(self, outputs):
"""Called at the end of validation to aggregate outputs.
Args:
outputs (list): The individual outputs of each validation step.
Returns:
dict: Validation loss and tensorboard logs.
"""
if outputs:
avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
perplexity = self.validation_perplexity.compute()
logging.info(f"evaluation perplexity {perplexity.cpu().item()}")
self.log(f'val_loss', avg_loss)
def setup_training_data(self, train_data_config: Optional[DictConfig]):
self._train_dl = (
self._setup_preprocessed_dataloader(train_data_config)
if self.tokenizer is None
else self._setup_dataloader(train_data_config)
)
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
self._validation_dl = (
self._setup_preprocessed_dataloader(val_data_config)
if self.tokenizer is None
else self._setup_dataloader(val_data_config)
)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
pass
def _setup_preprocessed_dataloader(self, cfg: Optional[DictConfig]):
dataset = cfg.data_file
max_predictions_per_seq = cfg.max_predictions_per_seq
batch_size = cfg.batch_size
if os.path.isdir(dataset):
files = [os.path.join(dataset, f) for f in os.listdir(dataset) if os.path.isfile(os.path.join(dataset, f))]
else:
files = [dataset]
files.sort()
dl = BertPretrainingPreprocessedDataloader(
data_files=files, max_predictions_per_seq=max_predictions_per_seq, batch_size=batch_size,
)
return dl
def _setup_tokenizer(self, cfg: DictConfig):
tokenizer = get_tokenizer(
tokenizer_name=cfg.tokenizer_name,
tokenizer_model=cfg.tokenizer_model,
special_tokens=OmegaConf.to_container(cfg.special_tokens) if cfg.special_tokens else None,
vocab_file=cfg.vocab_file,
)
self.tokenizer = tokenizer
def _setup_dataloader(self, cfg: DictConfig):
dataset = BertPretrainingDataset(
tokenizer=self.tokenizer,
data_file=cfg.data_file,
max_seq_length=cfg.max_seq_length,
mask_prob=cfg.mask_prob,
short_seq_prob=cfg.short_seq_prob,
)
dl = torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
collate_fn=dataset.collate_fn,
drop_last=cfg.get("drop_last", False),
shuffle=cfg.shuffle,
num_workers=cfg.get("num_workers", 0),
)
return dl
@classmethod
def list_available_models(cls) -> Optional[PretrainedModelInfo]:
"""
This method returns a list of pre-trained model which can be instantiated directly from NVIDIA's NGC cloud.
Returns:
List of available pre-trained models.
"""
result = []
result.append(
PretrainedModelInfo(
pretrained_model_name="bertbaseuncased",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/bertbaseuncased/versions/1.0.0rc1/files/bertbaseuncased.nemo",
description="The model was trained EN Wikipedia and BookCorpus on a sequence length of 512.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="bertlargeuncased",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/bertlargeuncased/versions/1.0.0rc1/files/bertlargeuncased.nemo",
description="The model was trained EN Wikipedia and BookCorpus on a sequence length of 512.",
)
)
return result
@@ -0,0 +1,276 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import math
from typing import Dict, Optional
import numpy as np
import torch
import torch.utils.data as pt_data
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from nemo.collections.common.losses import SmoothedCrossEntropyLoss
from nemo.collections.common.metrics import GlobalAverageLossMetric
from nemo.collections.common.parts import transformer_weights_init
from nemo.collections.nlp.data import SentenceDataset, TarredSentenceDataset
from nemo.collections.nlp.metrics import SequencePerplexity
from nemo.collections.nlp.modules.common import TokenClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_transformer
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer
from nemo.core.classes.common import typecheck
from nemo.core.classes.modelPT import ModelPT
from nemo.utils import logging, model_utils
__all__ = ["TransformerLMModel"]
class TransformerLMModel(ModelPT):
"""
Left-to-right Transformer language model.
"""
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
# Get global rank and total number of GPU workers for IterableDataset partitioning, if applicable
self.world_size = 1
if trainer is not None:
self.world_size = trainer.num_nodes * trainer.num_gpus
cfg = model_utils.convert_model_config_to_dict_config(cfg)
cfg = model_utils.maybe_update_config_version(cfg)
# Instantiates tokenizer and register to be saved with NeMo Model archive
# After this call, ther will be self.tokenizer which can convert between tokens and token_ids.
self.setup_tokenizer(
tokenizer_name=cfg.tokenizer.get("tokenizer_name", "yttm"),
tokenizer_model=cfg.tokenizer.get("tokenizer_model", None),
vocab_file=cfg.tokenizer.get("vocab_file", None),
bpe_dropout=cfg.tokenizer.get("bpe_dropout", 0.0),
)
# init superclass
super().__init__(cfg=cfg, trainer=trainer)
# make vocabulary size divisible by 8 for fast fp16 training
vocab_size = 8 * math.ceil(self.tokenizer.vocab_size / 8)
# encoder from NeMo, Megatron-LM, or HuggingFace
encoder_cfg_dict = OmegaConf.to_container(cfg.get('encoder'))
encoder_cfg_dict['vocab_size'] = vocab_size
library = encoder_cfg_dict.pop('library', 'nemo')
model_name = encoder_cfg_dict.pop('model_name', None)
pretrained = encoder_cfg_dict.pop('pretrained', False)
self.encoder = get_transformer(
library=library, model_name=model_name, pretrained=pretrained, config_dict=encoder_cfg_dict, encoder=True,
)
self.log_softmax = TokenClassifier(
hidden_size=self.encoder.hidden_size,
num_classes=vocab_size,
activation=cfg.head.activation,
log_softmax=cfg.head.log_softmax,
dropout=cfg.head.dropout,
use_transformer_init=cfg.head.use_transformer_init,
)
# tie weights of embedding and softmax matrices
self.log_softmax.mlp.layer0.weight = self.encoder.embedding.token_embedding.weight
std_init_range = 1 / self.encoder.hidden_size ** 0.5
# initialize weights if not using pretrained encoder
if not self._cfg.encoder.get('pretrained', False):
self.encoder.apply(lambda module: transformer_weights_init(module, std_init_range))
self.log_softmax.apply(lambda module: transformer_weights_init(module, std_init_range))
self.loss_fn = SmoothedCrossEntropyLoss(pad_id=self.tokenizer.pad_id, label_smoothing=cfg.label_smoothing)
self.eval_loss_fn = SmoothedCrossEntropyLoss(pad_id=self.tokenizer.pad_id)
self.eval_loss = GlobalAverageLossMetric(dist_sync_on_step=False, take_avg_loss=True)
self.eval_ppl = SequencePerplexity()
@typecheck()
def forward(self, input_ids, attention_mask):
"""
No special modification required for Lightning, define it as you normally would
in the `nn.Module` in vanilla PyTorch.
"""
hidden_states = self.encoder(input_ids=input_ids, encoder_mask=attention_mask)
log_probs = self.log_softmax(hidden_states=hidden_states)
return log_probs
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
# forward pass
for i in range(len(batch)):
if batch[i].ndim == 3:
# Dataset returns already batched data and the first dimension of size 1
# added by DataLoader is excess.
batch[i] = batch[i].squeeze(dim=0)
ids, mask = batch
input_ids, labels = ids[:, :-1], ids[:, 1:]
input_mask = mask[:, :-1]
log_probs = self(input_ids=input_ids, attention_mask=input_mask)
train_loss = self.loss_fn(log_probs=log_probs, labels=labels)
tensorboard_logs = {
"train_loss": train_loss,
"lr": self._optimizer.param_groups[0]["lr"],
}
return {"loss": train_loss, "log": tensorboard_logs}
def eval_step(self, batch, batch_idx):
for i in range(len(batch)):
if batch[i].ndim == 3:
# Dataset returns already batched data and the first dimension of size 1
# added by DataLoader is excess.
batch[i] = batch[i].squeeze(dim=0)
ids, mask = batch
input_ids, labels = ids[:, :-1], ids[:, 1:]
input_mask, output_mask = mask[:, :-1], mask[:, 1:]
log_probs = self(input_ids=input_ids, attention_mask=input_mask)
eval_loss = self.eval_loss_fn(log_probs=log_probs, labels=labels)
self.eval_loss(loss=eval_loss, num_measurements=log_probs.shape[0] * log_probs.shape[1])
self.eval_ppl(log_probs=log_probs, labels=labels, mask=output_mask)
return {}
def test_step(self, batch, batch_idx):
return self.eval_step(batch, batch_idx)
def validation_step(self, batch, batch_idx):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
return self.eval_step(batch, batch_idx)
def eval_epoch_end(self, outputs, mode):
eval_loss = self.eval_loss.compute()
eval_ppl = self.eval_ppl.compute()
ans = {f"{mode}_loss": eval_loss, f"{mode}_ppl": eval_ppl}
ans['log'] = dict(ans)
dataset_name = "Validation" if mode == 'val' else "Test"
logging.info(f"\n\n\n\n{dataset_name} PPL: {np.round(eval_ppl.item(), 2)}")
return ans
def validation_epoch_end(self, outputs):
"""
Called at the end of validation to aggregate outputs.
:param outputs: list of individual outputs of each validation step.
"""
self.log_dict(self.eval_epoch_end(outputs, 'val'), sync_dist=True)
self.eval_loss.reset()
self.eval_ppl.reset()
def test_epoch_end(self, outputs):
return self.eval_epoch_end(outputs, 'test')
def setup_tokenizer(
self, tokenizer_name=None, tokenizer_model=None, vocab_file=None, bpe_dropout=0.0,
):
supported_tokenizers = ['yttm', 'huggingface', 'sentencepiece', 'word']
if tokenizer_name not in supported_tokenizers:
raise NotImplementedError(f"Currently we only support tokenizers in {supported_tokenizers}.")
self.tokenizer = get_tokenizer(
tokenizer_name=tokenizer_name,
tokenizer_model=self.register_artifact("cfg.tokenizer.tokenizer_model", tokenizer_model),
vocab_file=vocab_file,
bpe_dropout=bpe_dropout,
special_tokens=None,
use_fast=False,
)
def setup_training_data(self, train_data_config: Optional[DictConfig]):
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config)
def _setup_dataloader_from_config(self, cfg: DictConfig, predict_last_k=0):
if cfg.get("use_tarred_dataset", False):
if cfg.get("metadata_file") is None:
raise FileNotFoundError("Trying to use tarred data set but could not find metadata path in config.")
else:
metadata_file = cfg.get('metadata_file')
with open(metadata_file) as metadata_reader:
metadata = json.load(metadata_reader)
if cfg.get('tar_files') is None:
tar_files = metadata.get('tar_files')
if tar_files is not None:
logging.info(f'Loading from tarred dataset {tar_files}')
else:
raise FileNotFoundError("Could not find tarred dataset in config or metadata.")
else:
tar_files = cfg.get('tar_files')
if metadata.get('tar_files') is not None:
raise ValueError(
'Tar files specified in config and in metadata file. Tar files should only be specified once.'
)
dataset = TarredSentenceDataset(
text_tar_filepaths=tar_files,
metadata_path=metadata_file,
tokenizer=self.tokenizer,
shuffle_n=cfg.get("tar_shuffle_n", 100),
shard_strategy=cfg.get("shard_strategy", "scatter"),
global_rank=self.global_rank,
world_size=self.world_size,
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=1,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
else:
dataset = SentenceDataset(
tokenizer=self.tokenizer,
dataset=cfg.file_name,
tokens_in_batch=cfg.tokens_in_batch,
clean=cfg.get("clean", False),
max_seq_length=cfg.get("max_seq_length", 512),
min_seq_length=cfg.get("min_seq_length", 1),
cache_ids=cfg.get("cache_ids", False),
)
if cfg.shuffle:
sampler = pt_data.RandomSampler(dataset)
else:
sampler = pt_data.SequentialSampler(dataset)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=1,
sampler=sampler,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
pass
@@ -0,0 +1,15 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.machine_translation.mt_enc_dec_model import MTEncDecModel
@@ -0,0 +1,134 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from typing import Any, Optional, Tuple
from omegaconf.omegaconf import MISSING
from nemo.collections.nlp.data.machine_translation.machine_translation_dataset import TranslationDataConfig
from nemo.collections.nlp.models.enc_dec_nlp_model import EncDecNLPModelConfig
from nemo.collections.nlp.modules.common.token_classifier import TokenClassifierConfig
from nemo.collections.nlp.modules.common.tokenizer_utils import TokenizerConfig
from nemo.collections.nlp.modules.common.transformer.transformer import (
NeMoTransformerConfig,
NeMoTransformerEncoderConfig,
)
from nemo.core.config.modelPT import ModelConfig, OptimConfig, SchedConfig
@dataclass
class MTSchedConfig(SchedConfig):
name: str = 'InverseSquareRootAnnealing'
warmup_ratio: Optional[float] = None
last_epoch: int = -1
# TODO: Refactor this dataclass to to support more optimizers (it pins the optimizer to Adam-like optimizers).
@dataclass
class MTOptimConfig(OptimConfig):
name: str = 'adam'
lr: float = 1e-3
betas: Tuple[float, float] = (0.9, 0.98)
weight_decay: float = 0.0
sched: Optional[MTSchedConfig] = MTSchedConfig()
@dataclass
class MTEncDecModelConfig(EncDecNLPModelConfig):
# machine translation configurations
num_val_examples: int = 3
num_test_examples: int = 3
max_generation_delta: int = 10
label_smoothing: Optional[float] = 0.0
beam_size: int = 4
len_pen: float = 0.0
src_language: Any = 'en' # Any = str or List[str]
tgt_language: Any = 'en' # Any = str or List[str]
find_unused_parameters: Optional[bool] = True
shared_tokenizer: Optional[bool] = True
multilingual: Optional[bool] = False
preproc_out_dir: Optional[str] = None
# network architecture configuration
encoder_tokenizer: Any = MISSING
encoder: Any = MISSING
decoder_tokenizer: Any = MISSING
decoder: Any = MISSING
head: TokenClassifierConfig = TokenClassifierConfig(log_softmax=True)
# dataset configurations
train_ds: Optional[TranslationDataConfig] = TranslationDataConfig(
src_file_name=MISSING,
tgt_file_name=MISSING,
tokens_in_batch=512,
clean=True,
shuffle=True,
cache_ids=False,
use_cache=False,
)
validation_ds: Optional[TranslationDataConfig] = TranslationDataConfig(
src_file_name=MISSING,
tgt_file_name=MISSING,
tokens_in_batch=512,
clean=False,
shuffle=False,
cache_ids=False,
use_cache=False,
)
test_ds: Optional[TranslationDataConfig] = TranslationDataConfig(
src_file_name=MISSING,
tgt_file_name=MISSING,
tokens_in_batch=512,
clean=False,
shuffle=False,
cache_ids=False,
use_cache=False,
)
optim: Optional[OptimConfig] = MTOptimConfig()
@dataclass
class AAYNBaseConfig(MTEncDecModelConfig):
# Attention is All You Need Base Configuration
encoder_tokenizer: TokenizerConfig = TokenizerConfig(library='yttm')
decoder_tokenizer: TokenizerConfig = TokenizerConfig(library='yttm')
encoder: NeMoTransformerEncoderConfig = NeMoTransformerEncoderConfig(
library='nemo',
model_name=None,
pretrained=False,
hidden_size=512,
inner_size=2048,
num_layers=6,
num_attention_heads=8,
ffn_dropout=0.1,
attn_score_dropout=0.1,
attn_layer_dropout=0.1,
)
decoder: NeMoTransformerConfig = NeMoTransformerConfig(
library='nemo',
model_name=None,
pretrained=False,
inner_size=2048,
num_layers=6,
num_attention_heads=8,
ffn_dropout=0.1,
attn_score_dropout=0.1,
attn_layer_dropout=0.1,
)
@@ -0,0 +1,836 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import itertools
import json
import random
from multiprocessing import Value
from pathlib import Path
from typing import Dict, List, Optional, Union
import numpy as np
import torch
import torch.distributed as dist
import torch.utils.data as pt_data
from omegaconf import DictConfig, ListConfig, OmegaConf
from pytorch_lightning import Trainer
from pytorch_lightning.utilities import rank_zero_only
from sacrebleu import corpus_bleu
from nemo.collections.common.data import ConcatDataset
from nemo.collections.common.losses import NLLLoss, SmoothedCrossEntropyLoss
from nemo.collections.common.metrics import GlobalAverageLossMetric
from nemo.collections.common.parts import transformer_weights_init
from nemo.collections.common.tokenizers.chinese_tokenizers import ChineseProcessor
from nemo.collections.common.tokenizers.en_ja_tokenizers import EnJaProcessor
from nemo.collections.common.tokenizers.moses_tokenizers import MosesProcessor
from nemo.collections.nlp.data import TarredTranslationDataset, TranslationDataset
from nemo.collections.nlp.models.enc_dec_nlp_model import EncDecNLPModel
from nemo.collections.nlp.models.machine_translation.mt_enc_dec_config import MTEncDecModelConfig
from nemo.collections.nlp.modules.common import TokenClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_transformer
from nemo.collections.nlp.modules.common.tokenizer_utils import get_nmt_tokenizer
from nemo.collections.nlp.modules.common.transformer import BeamSearchSequenceGenerator, TopKSequenceGenerator
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.utils import logging, model_utils
__all__ = ['MTEncDecModel']
class MTEncDecModel(EncDecNLPModel):
"""
Encoder-decoder machine translation model.
"""
def __init__(self, cfg: MTEncDecModelConfig, trainer: Trainer = None):
cfg = model_utils.convert_model_config_to_dict_config(cfg)
# Get global rank and total number of GPU workers for IterableDataset partitioning, if applicable
# Global_rank and local_rank is set by LightningModule in Lightning 1.2.0
self.world_size = 1
if trainer is not None:
self.world_size = trainer.num_nodes * trainer.num_gpus
cfg = model_utils.maybe_update_config_version(cfg)
self.src_language = cfg.get("src_language", None)
self.tgt_language = cfg.get("tgt_language", None)
self.multilingual = cfg.get("multilingual", False)
self.multilingual_ids = []
# Instantiates tokenizers and register to be saved with NeMo Model archive
# After this call, ther will be self.encoder_tokenizer and self.decoder_tokenizer
# Which can convert between tokens and token_ids for SRC and TGT languages correspondingly.
self.setup_enc_dec_tokenizers(
encoder_tokenizer_library=cfg.encoder_tokenizer.get('library', 'yttm'),
encoder_tokenizer_model=cfg.encoder_tokenizer.get('tokenizer_model'),
encoder_bpe_dropout=cfg.encoder_tokenizer.get('bpe_dropout', 0.0)
if cfg.encoder_tokenizer.get('bpe_dropout', 0.0) is not None
else 0.0,
encoder_model_name=cfg.encoder.get('model_name') if hasattr(cfg.encoder, 'model_name') else None,
decoder_tokenizer_library=cfg.decoder_tokenizer.get('library', 'yttm'),
decoder_tokenizer_model=cfg.decoder_tokenizer.tokenizer_model,
decoder_bpe_dropout=cfg.decoder_tokenizer.get('bpe_dropout', 0.0)
if cfg.decoder_tokenizer.get('bpe_dropout', 0.0) is not None
else 0.0,
decoder_model_name=cfg.decoder.get('model_name') if hasattr(cfg.decoder, 'model_name') else None,
)
if self.multilingual:
if isinstance(self.src_language, ListConfig) and isinstance(self.tgt_language, ListConfig):
raise ValueError(
"cfg.src_language and cfg.tgt_language cannot both be lists. We only support many-to-one or one-to-many multilingual models."
)
elif isinstance(self.src_language, ListConfig):
for lng in self.src_language:
self.multilingual_ids.append(self.encoder_tokenizer.token_to_id("<" + lng + ">"))
elif isinstance(self.tgt_language, ListConfig):
for lng in self.tgt_language:
self.multilingual_ids.append(self.encoder_tokenizer.token_to_id("<" + lng + ">"))
else:
raise ValueError(
"Expect either cfg.src_language or cfg.tgt_language to be a list when multilingual=True."
)
if isinstance(self.src_language, ListConfig):
self.tgt_language = [self.tgt_language] * len(self.src_language)
else:
self.src_language = [self.src_language] * len(self.tgt_language)
self.source_processor_list = []
self.target_processor_list = []
for src_lng, tgt_lng in zip(self.src_language, self.tgt_language):
src_prcsr, tgt_prscr = self.setup_pre_and_post_processing_utils(
source_lang=src_lng, target_lang=tgt_lng
)
self.source_processor_list.append(src_prcsr)
self.target_processor_list.append(tgt_prscr)
else:
# After this call, the model will have self.source_processor and self.target_processor objects
self.setup_pre_and_post_processing_utils(source_lang=self.src_language, target_lang=self.tgt_language)
self.multilingual_ids = [None]
# TODO: Why is this base constructor call so late in the game?
super().__init__(cfg=cfg, trainer=trainer)
# encoder from NeMo, Megatron-LM, or HuggingFace
encoder_cfg_dict = OmegaConf.to_container(cfg.get('encoder'))
encoder_cfg_dict['vocab_size'] = self.encoder_vocab_size
library = encoder_cfg_dict.pop('library', 'nemo')
model_name = encoder_cfg_dict.pop('model_name', None)
pretrained = encoder_cfg_dict.pop('pretrained', False)
self.encoder = get_transformer(
library=library,
model_name=model_name,
pretrained=pretrained,
config_dict=encoder_cfg_dict,
encoder=True,
pre_ln_final_layer_norm=encoder_cfg_dict.get('pre_ln_final_layer_norm', False),
)
# decoder from NeMo, Megatron-LM, or HuggingFace
decoder_cfg_dict = OmegaConf.to_container(cfg.get('decoder'))
decoder_cfg_dict['vocab_size'] = self.decoder_vocab_size
library = decoder_cfg_dict.pop('library', 'nemo')
model_name = decoder_cfg_dict.pop('model_name', None)
pretrained = decoder_cfg_dict.pop('pretrained', False)
decoder_cfg_dict['hidden_size'] = self.encoder.hidden_size
self.decoder = get_transformer(
library=library,
model_name=model_name,
pretrained=pretrained,
config_dict=decoder_cfg_dict,
encoder=False,
pre_ln_final_layer_norm=decoder_cfg_dict.get('pre_ln_final_layer_norm', False),
)
self.log_softmax = TokenClassifier(
hidden_size=self.decoder.hidden_size,
num_classes=self.decoder_vocab_size,
activation=cfg.head.activation,
log_softmax=cfg.head.log_softmax,
dropout=cfg.head.dropout,
use_transformer_init=cfg.head.use_transformer_init,
)
self.beam_search = BeamSearchSequenceGenerator(
embedding=self.decoder.embedding,
decoder=self.decoder.decoder,
log_softmax=self.log_softmax,
max_sequence_length=self.decoder.max_sequence_length,
beam_size=cfg.beam_size,
bos=self.decoder_tokenizer.bos_id,
pad=self.decoder_tokenizer.pad_id,
eos=self.decoder_tokenizer.eos_id,
len_pen=cfg.len_pen,
max_delta_length=cfg.max_generation_delta,
)
# tie weights of embedding and softmax matrices
self.log_softmax.mlp.layer0.weight = self.decoder.embedding.token_embedding.weight
# TODO: encoder and decoder with different hidden size?
std_init_range = 1 / self.encoder.hidden_size ** 0.5
# initialize weights if not using pretrained encoder/decoder
if not self._cfg.encoder.get('pretrained', False):
self.encoder.apply(lambda module: transformer_weights_init(module, std_init_range))
if not self._cfg.decoder.get('pretrained', False):
self.decoder.apply(lambda module: transformer_weights_init(module, std_init_range))
self.log_softmax.apply(lambda module: transformer_weights_init(module, std_init_range))
self.loss_fn = SmoothedCrossEntropyLoss(
pad_id=self.decoder_tokenizer.pad_id, label_smoothing=cfg.label_smoothing
)
self.eval_loss_fn = NLLLoss(ignore_index=self.decoder_tokenizer.pad_id)
def filter_predicted_ids(self, ids):
ids[ids >= self.decoder_tokenizer.vocab_size] = self.decoder_tokenizer.unk_id
return ids
@typecheck()
def forward(self, src, src_mask, tgt, tgt_mask):
src_hiddens = self.encoder(input_ids=src, encoder_mask=src_mask)
tgt_hiddens = self.decoder(
input_ids=tgt, decoder_mask=tgt_mask, encoder_embeddings=src_hiddens, encoder_mask=src_mask
)
log_probs = self.log_softmax(hidden_states=tgt_hiddens)
return log_probs
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
# forward pass
for i in range(len(batch)):
if batch[i].ndim == 3:
# Dataset returns already batched data and the first dimension of size 1 added by DataLoader
# is excess.
batch[i] = batch[i].squeeze(dim=0)
src_ids, src_mask, tgt_ids, tgt_mask, labels = batch
log_probs = self(src_ids, src_mask, tgt_ids, tgt_mask)
train_loss = self.loss_fn(log_probs=log_probs, labels=labels)
tensorboard_logs = {
'train_loss': train_loss,
'lr': self._optimizer.param_groups[0]['lr'],
}
return {'loss': train_loss, 'log': tensorboard_logs}
def eval_step(self, batch, batch_idx, mode, dataloader_idx=0):
for i in range(len(batch)):
if batch[i].ndim == 3:
# Dataset returns already batched data and the first dimension of size 1 added by DataLoader
# is excess.
batch[i] = batch[i].squeeze(dim=0)
if self.multilingual:
self.source_processor = self.source_processor_list[dataloader_idx]
self.target_processor = self.target_processor_list[dataloader_idx]
src_ids, src_mask, tgt_ids, tgt_mask, labels = batch
log_probs = self(src_ids, src_mask, tgt_ids, tgt_mask)
eval_loss = self.eval_loss_fn(log_probs=log_probs, labels=labels)
# this will run encoder twice -- TODO: potentially fix
_, translations = self.batch_translate(src=src_ids, src_mask=src_mask)
if dataloader_idx == 0:
getattr(self, f'{mode}_loss')(loss=eval_loss, num_measurements=log_probs.shape[0] * log_probs.shape[1])
else:
getattr(self, f'{mode}_loss_{dataloader_idx}')(
loss=eval_loss, num_measurements=log_probs.shape[0] * log_probs.shape[1]
)
np_tgt = tgt_ids.detach().cpu().numpy()
ground_truths = [self.decoder_tokenizer.ids_to_text(tgt) for tgt in np_tgt]
ground_truths = [self.target_processor.detokenize(tgt.split(' ')) for tgt in ground_truths]
num_non_pad_tokens = np.not_equal(np_tgt, self.decoder_tokenizer.pad_id).sum().item()
return {
'translations': translations,
'ground_truths': ground_truths,
'num_non_pad_tokens': num_non_pad_tokens,
}
def test_step(self, batch, batch_idx, dataloader_idx=0):
return self.eval_step(batch, batch_idx, 'test', dataloader_idx)
@rank_zero_only
def log_param_stats(self):
for name, p in self.named_parameters():
if p.requires_grad:
self.trainer.logger.experiment.add_histogram(name + '_hist', p, global_step=self.global_step)
self.trainer.logger.experiment.add_scalars(
name,
{'mean': p.mean(), 'stddev': p.std(), 'max': p.max(), 'min': p.min()},
global_step=self.global_step,
)
def validation_step(self, batch, batch_idx, dataloader_idx=0):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
return self.eval_step(batch, batch_idx, 'val', dataloader_idx)
def eval_epoch_end(self, outputs, mode):
# if user specifies one validation dataloader, then PTL reverts to giving a list of dictionary instead of a list of list of dictionary
if isinstance(outputs[0], dict):
outputs = [outputs]
loss_list = []
sb_score_list = []
for dataloader_idx, output in enumerate(outputs):
if dataloader_idx == 0:
eval_loss = getattr(self, f'{mode}_loss').compute()
else:
eval_loss = getattr(self, f'{mode}_loss_{dataloader_idx}').compute()
translations = list(itertools.chain(*[x['translations'] for x in output]))
ground_truths = list(itertools.chain(*[x['ground_truths'] for x in output]))
assert len(translations) == len(ground_truths)
# Gather translations and ground truths from all workers
tr_and_gt = [None for _ in range(self.world_size)]
# we also need to drop pairs where ground truth is an empty string
dist.all_gather_object(
tr_and_gt, [(t, g) for (t, g) in zip(translations, ground_truths) if g.strip() != '']
)
if self.global_rank == 0:
_translations = []
_ground_truths = []
for rank in range(0, self.world_size):
_translations += [t for (t, g) in tr_and_gt[rank]]
_ground_truths += [g for (t, g) in tr_and_gt[rank]]
if self.tgt_language in ['ja']:
sacre_bleu = corpus_bleu(_translations, [_ground_truths], tokenize="ja-mecab")
elif self.tgt_language in ['zh']:
sacre_bleu = corpus_bleu(_translations, [_ground_truths], tokenize="zh")
else:
sacre_bleu = corpus_bleu(_translations, [_ground_truths], tokenize="13a")
# because the reduction op later is average (over word_size)
sb_score = sacre_bleu.score * self.world_size
dataset_name = "Validation" if mode == 'val' else "Test"
logging.info(
f"Dataset name: {dataset_name}, Dataloader index: {dataloader_idx}, Set size: {len(translations)}"
)
logging.info(
f"Dataset name: {dataset_name}, Dataloader index: {dataloader_idx}, Val Loss = {eval_loss}"
)
logging.info(
f"Dataset name: {dataset_name}, Dataloader index: {dataloader_idx}, Sacre BLEU = {sb_score / self.world_size}"
)
logging.info(
f"Dataset name: {dataset_name}, Dataloader index: {dataloader_idx}, Translation Examples:"
)
for i in range(0, 3):
ind = random.randint(0, len(translations) - 1)
logging.info(" " + '\u0332'.join(f"Example {i}:"))
logging.info(f" Prediction: {translations[ind]}")
logging.info(f" Ground Truth: {ground_truths[ind]}")
else:
sb_score = 0.0
loss_list.append(eval_loss.cpu().numpy())
sb_score_list.append(sb_score)
if dataloader_idx == 0:
self.log(f"{mode}_loss", eval_loss, sync_dist=True)
self.log(f"{mode}_sacreBLEU", sb_score, sync_dist=True)
getattr(self, f'{mode}_loss').reset()
else:
self.log(f"{mode}_loss_dl_index_{dataloader_idx}", eval_loss, sync_dist=True)
self.log(f"{mode}_sacreBLEU_dl_index_{dataloader_idx}", sb_score, sync_dist=True)
getattr(self, f'{mode}_loss_{dataloader_idx}').reset()
if len(loss_list) > 1:
self.log(f"{mode}_loss_avg", np.mean(loss_list), sync_dist=True)
self.log(f"{mode}_sacreBLEU_avg", np.mean(sb_score_list), sync_dist=True)
def validation_epoch_end(self, outputs):
"""
Called at the end of validation to aggregate outputs.
:param outputs: list of individual outputs of each validation step.
"""
self.eval_epoch_end(outputs, 'val')
def test_epoch_end(self, outputs):
self.eval_epoch_end(outputs, 'test')
def setup_enc_dec_tokenizers(
self,
encoder_tokenizer_library=None,
encoder_tokenizer_model=None,
encoder_bpe_dropout=0.0,
encoder_model_name=None,
decoder_tokenizer_library=None,
decoder_tokenizer_model=None,
decoder_bpe_dropout=0.0,
decoder_model_name=None,
):
supported_tokenizers = ['yttm', 'huggingface', 'sentencepiece']
if (
encoder_tokenizer_library not in supported_tokenizers
or decoder_tokenizer_library not in supported_tokenizers
):
raise NotImplementedError(f"Currently we only support tokenizers in {supported_tokenizers}.")
self.encoder_tokenizer = get_nmt_tokenizer(
library=encoder_tokenizer_library,
tokenizer_model=self.register_artifact("encoder_tokenizer.tokenizer_model", encoder_tokenizer_model),
bpe_dropout=encoder_bpe_dropout,
model_name=encoder_model_name,
vocab_file=None,
special_tokens=None,
use_fast=False,
)
self.decoder_tokenizer = get_nmt_tokenizer(
library=decoder_tokenizer_library,
tokenizer_model=self.register_artifact("decoder_tokenizer.tokenizer_model", decoder_tokenizer_model),
bpe_dropout=decoder_bpe_dropout,
model_name=decoder_model_name,
vocab_file=None,
special_tokens=None,
use_fast=False,
)
def setup_training_data(self, train_data_config: Optional[DictConfig]):
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
def setup_multiple_validation_data(self, val_data_config: Union[DictConfig, Dict]):
self.setup_validation_data(self._cfg.get('validation_ds'))
def setup_multiple_test_data(self, test_data_config: Union[DictConfig, Dict]):
self.setup_test_data(self._cfg.get('test_ds'))
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
self._validation_dl = self._setup_eval_dataloader_from_config(cfg=val_data_config)
# instantiate Torchmetric for each val dataloader
if self._validation_dl is not None:
for dataloader_idx in range(len(self._validation_dl)):
if dataloader_idx == 0:
setattr(
self, f'val_loss', GlobalAverageLossMetric(dist_sync_on_step=False, take_avg_loss=True),
)
else:
setattr(
self,
f'val_loss_{dataloader_idx}',
GlobalAverageLossMetric(dist_sync_on_step=False, take_avg_loss=True),
)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
self._test_dl = self._setup_eval_dataloader_from_config(cfg=test_data_config)
# instantiate Torchmetric for each test dataloader
if self._test_dl is not None:
for dataloader_idx in range(len(self._test_dl)):
if dataloader_idx == 0:
setattr(
self, f'test_loss', GlobalAverageLossMetric(dist_sync_on_step=False, take_avg_loss=True),
)
else:
setattr(
self,
f'test_loss_{dataloader_idx}',
GlobalAverageLossMetric(dist_sync_on_step=False, take_avg_loss=True),
)
def _setup_dataloader_from_config(self, cfg: DictConfig):
if cfg.get("use_tarred_dataset", False):
if cfg.get("metadata_file") is None:
raise FileNotFoundError("Trying to use tarred data set but could not find metadata path in config.")
else:
if not self.multilingual:
metadata_file_list = [cfg.get('metadata_file')]
else:
metadata_file_list = cfg.get('metadata_file')
datasets = []
for idx, metadata_file in enumerate(metadata_file_list):
with open(metadata_file) as metadata_reader:
metadata = json.load(metadata_reader)
if cfg.get('tar_files') is None:
tar_files = metadata.get('tar_files')
if tar_files is not None:
logging.info(f'Loading from tarred dataset {tar_files}')
else:
raise FileNotFoundError("Could not find tarred dataset in config or metadata.")
else:
tar_files = cfg.get('tar_files')
if self.multilingual:
tar_files = tar_files[idx]
if metadata.get('tar_files') is not None:
logging.info(
f'Tar file paths found in both cfg and metadata using one in cfg by default - {tar_files}'
)
dataset = TarredTranslationDataset(
text_tar_filepaths=tar_files,
metadata_path=metadata_file,
encoder_tokenizer=self.encoder_tokenizer,
decoder_tokenizer=self.decoder_tokenizer,
shuffle_n=cfg.get("tar_shuffle_n", 100),
shard_strategy=cfg.get("shard_strategy", "scatter"),
global_rank=self.global_rank,
world_size=self.world_size,
reverse_lang_direction=cfg.get("reverse_lang_direction", False),
prepend_id=self.multilingual_ids[idx],
)
datasets.append(dataset)
if self.multilingual:
dataset = ConcatDataset(
datasets=datasets,
sampling_technique=cfg.get('concat_sampling_technique'),
sampling_temperature=cfg.get('concat_sampling_temperature'),
sampling_probabilities=cfg.get('concat_sampling_probabilities'),
global_rank=self.global_rank,
world_size=self.world_size,
)
else:
dataset = datasets[0]
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=1,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
else:
if not self.multilingual:
src_file_list = [cfg.src_file_name]
tgt_file_list = [cfg.tgt_file_name]
else:
src_file_list = cfg.src_file_name
tgt_file_list = cfg.tgt_file_name
if len(src_file_list) != len(tgt_file_list):
raise ValueError(
'The same number of filepaths must be passed in for source and target while training multilingual.'
)
datasets = []
for idx, src_file in enumerate(src_file_list):
dataset = TranslationDataset(
dataset_src=str(Path(src_file).expanduser()),
dataset_tgt=str(Path(tgt_file_list[idx]).expanduser()),
tokens_in_batch=cfg.tokens_in_batch,
clean=cfg.get("clean", False),
max_seq_length=cfg.get("max_seq_length", 512),
min_seq_length=cfg.get("min_seq_length", 1),
max_seq_length_diff=cfg.get("max_seq_length_diff", 512),
max_seq_length_ratio=cfg.get("max_seq_length_ratio", 512),
cache_ids=cfg.get("cache_ids", False),
cache_data_per_node=cfg.get("cache_data_per_node", False),
use_cache=cfg.get("use_cache", False),
reverse_lang_direction=cfg.get("reverse_lang_direction", False),
prepend_id=self.multilingual_ids[idx],
)
dataset.batchify(self.encoder_tokenizer, self.decoder_tokenizer)
datasets.append(dataset)
if self.multilingual:
dataset = ConcatDataset(
datasets=datasets,
shuffle=cfg.get('shuffle'),
sampling_technique=cfg.get('concat_sampling_technique'),
sampling_temperature=cfg.get('concat_sampling_temperature'),
sampling_probabilities=cfg.get('concat_sampling_probabilities'),
global_rank=self.global_rank,
world_size=self.world_size,
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=1,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
else:
dataset = datasets[0]
if cfg.shuffle:
sampler = pt_data.RandomSampler(dataset)
else:
sampler = pt_data.SequentialSampler(dataset)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=1,
sampler=sampler,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
def replace_beam_with_sampling(self, topk=500):
self.beam_search = TopKSequenceGenerator(
embedding=self.decoder.embedding,
decoder=self.decoder.decoder,
log_softmax=self.log_softmax,
max_sequence_length=self.beam_search.max_seq_length,
beam_size=topk,
bos=self.decoder_tokenizer.bos_id,
pad=self.decoder_tokenizer.pad_id,
eos=self.decoder_tokenizer.eos_id,
)
def _setup_eval_dataloader_from_config(self, cfg: DictConfig):
src_file_name = cfg.get('src_file_name')
tgt_file_name = cfg.get('tgt_file_name')
if src_file_name is None or tgt_file_name is None:
raise ValueError(
'Validation dataloader needs both cfg.src_file_name and cfg.tgt_file_name to not be None.'
)
else:
# convert src_file_name and tgt_file_name to list of strings
if isinstance(src_file_name, str):
src_file_list = [src_file_name]
elif isinstance(src_file_name, ListConfig):
src_file_list = src_file_name
else:
raise ValueError("cfg.src_file_name must be string or list of strings")
if isinstance(tgt_file_name, str):
tgt_file_list = [tgt_file_name]
elif isinstance(tgt_file_name, ListConfig):
tgt_file_list = tgt_file_name
else:
raise ValueError("cfg.tgt_file_name must be string or list of strings")
if len(src_file_list) != len(tgt_file_list):
raise ValueError('The same number of filepaths must be passed in for source and target validation.')
dataloaders = []
prepend_idx = 0
for idx, src_file in enumerate(src_file_list):
if self.multilingual:
prepend_idx = idx
dataset = TranslationDataset(
dataset_src=str(Path(src_file).expanduser()),
dataset_tgt=str(Path(tgt_file_list[idx]).expanduser()),
tokens_in_batch=cfg.tokens_in_batch,
clean=cfg.get("clean", False),
max_seq_length=cfg.get("max_seq_length", 512),
min_seq_length=cfg.get("min_seq_length", 1),
max_seq_length_diff=cfg.get("max_seq_length_diff", 512),
max_seq_length_ratio=cfg.get("max_seq_length_ratio", 512),
cache_ids=cfg.get("cache_ids", False),
cache_data_per_node=cfg.get("cache_data_per_node", False),
use_cache=cfg.get("use_cache", False),
reverse_lang_direction=cfg.get("reverse_lang_direction", False),
prepend_id=self.multilingual_ids[prepend_idx],
)
dataset.batchify(self.encoder_tokenizer, self.decoder_tokenizer)
if cfg.shuffle:
sampler = pt_data.RandomSampler(dataset)
else:
sampler = pt_data.SequentialSampler(dataset)
dataloader = torch.utils.data.DataLoader(
dataset=dataset,
batch_size=1,
sampler=sampler,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
)
dataloaders.append(dataloader)
return dataloaders
def setup_pre_and_post_processing_utils(self, source_lang, target_lang):
"""
Creates source and target processor objects for input and output pre/post-processing.
"""
self.source_processor, self.target_processor = None, None
if (source_lang == 'en' and target_lang == 'ja') or (source_lang == 'ja' and target_lang == 'en'):
self.source_processor = EnJaProcessor(source_lang)
self.target_processor = EnJaProcessor(target_lang)
else:
if source_lang == 'zh':
self.source_processor = ChineseProcessor()
if target_lang == 'zh':
self.target_processor = ChineseProcessor()
if source_lang is not None and source_lang not in ['ja', 'zh']:
self.source_processor = MosesProcessor(source_lang)
if target_lang is not None and target_lang not in ['ja', 'zh']:
self.target_processor = MosesProcessor(target_lang)
return self.source_processor, self.target_processor
@torch.no_grad()
def batch_translate(
self, src: torch.LongTensor, src_mask: torch.LongTensor,
):
"""
Translates a minibatch of inputs from source language to target language.
Args:
src: minibatch of inputs in the src language (batch x seq_len)
src_mask: mask tensor indicating elements to be ignored (batch x seq_len)
Returns:
translations: a list strings containing detokenized translations
inputs: a list of string containing detokenized inputs
"""
mode = self.training
try:
self.eval()
src_hiddens = self.encoder(input_ids=src, encoder_mask=src_mask)
beam_results = self.beam_search(encoder_hidden_states=src_hiddens, encoder_input_mask=src_mask)
beam_results = self.filter_predicted_ids(beam_results)
translations = [self.decoder_tokenizer.ids_to_text(tr) for tr in beam_results.cpu().numpy()]
inputs = [self.encoder_tokenizer.ids_to_text(inp) for inp in src.cpu().numpy()]
if self.target_processor is not None:
translations = [
self.target_processor.detokenize(translation.split(' ')) for translation in translations
]
if self.source_processor is not None:
inputs = [self.source_processor.detokenize(item.split(' ')) for item in inputs]
finally:
self.train(mode=mode)
return inputs, translations
# TODO: We should drop source/target_lang arguments in favor of using self.src/tgt_language
@torch.no_grad()
def translate(self, text: List[str], source_lang: str = None, target_lang: str = None) -> List[str]:
"""
Translates list of sentences from source language to target language.
Should be regular text, this method performs its own tokenization/de-tokenization
Args:
text: list of strings to translate
source_lang: if not None, corresponding MosesTokenizer and MosesPunctNormalizer will be run
target_lang: if not None, corresponding MosesDecokenizer will be run
Returns:
list of translated strings
"""
# __TODO__: This will reset both source and target processors even if you want to reset just one.
if source_lang is not None or target_lang is not None:
self.setup_pre_and_post_processing_utils(source_lang, target_lang)
mode = self.training
prepend_ids = []
if self.multilingual:
if source_lang is None or target_lang is None:
raise ValueError("Expect source_lang and target_lang to infer for multilingual model.")
src_symbol = self.encoder_tokenizer.token_to_id('<' + source_lang + '>')
tgt_symbol = self.encoder_tokenizer.token_to_id('<' + target_lang + '>')
prepend_ids = [src_symbol if src_symbol in self.multilingual_ids else tgt_symbol]
try:
self.eval()
inputs = []
for txt in text:
if self.source_processor is not None:
txt = self.source_processor.normalize(txt)
txt = self.source_processor.tokenize(txt)
ids = self.encoder_tokenizer.text_to_ids(txt)
ids = prepend_ids + [self.encoder_tokenizer.bos_id] + ids + [self.encoder_tokenizer.eos_id]
inputs.append(ids)
max_len = max(len(txt) for txt in inputs)
src_ids_ = np.ones((len(inputs), max_len)) * self.encoder_tokenizer.pad_id
for i, txt in enumerate(inputs):
src_ids_[i][: len(txt)] = txt
src_mask = torch.FloatTensor((src_ids_ != self.encoder_tokenizer.pad_id)).to(self.device)
src = torch.LongTensor(src_ids_).to(self.device)
_, translations = self.batch_translate(src, src_mask)
finally:
self.train(mode=mode)
return translations
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
"""
This method returns a list of pre-trained model which can be instantiated directly from NVIDIA's NGC cloud.
Returns:
List of available pre-trained models.
"""
result = []
model = PretrainedModelInfo(
pretrained_model_name="nmt_en_de_transformer12x2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_en_de_transformer12x2/versions/1.0.0rc1/files/nmt_en_de_transformer12x2.nemo",
description="En->De translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_en_de_transformer12x2",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_de_en_transformer12x2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_de_en_transformer12x2/versions/1.0.0rc1/files/nmt_de_en_transformer12x2.nemo",
description="De->En translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_de_en_transformer12x2",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_en_es_transformer12x2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_en_es_transformer12x2/versions/1.0.0rc1/files/nmt_en_es_transformer12x2.nemo",
description="En->Es translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_en_es_transformer12x2",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_es_en_transformer12x2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_es_en_transformer12x2/versions/1.0.0rc1/files/nmt_es_en_transformer12x2.nemo",
description="Es->En translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_es_en_transformer12x2",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_en_fr_transformer12x2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_en_fr_transformer12x2/versions/1.0.0rc1/files/nmt_en_fr_transformer12x2.nemo",
description="En->Fr translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_en_fr_transformer12x2",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_fr_en_transformer12x2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_fr_en_transformer12x2/versions/1.0.0rc1/files/nmt_fr_en_transformer12x2.nemo",
description="Fr->En translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_fr_en_transformer12x2",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_en_ru_transformer6x6",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_en_ru_transformer6x6/versions/1.0.0rc1/files/nmt_en_ru_transformer6x6.nemo",
description="En->Ru translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_en_ru_transformer6x6",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_ru_en_transformer6x6",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_ru_en_transformer6x6/versions/1.0.0rc1/files/nmt_ru_en_transformer6x6.nemo",
description="Ru->En translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_ru_en_transformer6x6",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_zh_en_transformer6x6",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_zh_en_transformer6x6/versions/1.0.0rc1/files/nmt_zh_en_transformer6x6.nemo",
description="Zh->En translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_zh_en_transformer6x6",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="nmt_en_zh_transformer6x6",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/nmt_en_zh_transformer6x6/versions/1.0.0rc1/files/nmt_en_zh_transformer6x6.nemo",
description="En->Zh translation model. See details here: https://ngc.nvidia.com/catalog/models/nvidia:nemo:nmt_en_zh_transformer6x6",
)
result.append(model)
return result
@@ -0,0 +1,17 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.neural_machine_translation.neural_machine_translation_model import (
NeuralMachineTranslationModel,
)
@@ -0,0 +1,245 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, MeetKai Inc. All rights reserved.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, List, Optional, Tuple, Union
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from transformers import AutoModel, BartForConditionalGeneration, EncoderDecoderModel
from nemo.collections.common.metrics import Perplexity
from nemo.collections.nlp.data.neural_machine_translation import NeuralMachineTranslationDataset
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer
from nemo.core.classes.common import typecheck
from nemo.core.classes.modelPT import ModelPT
from nemo.core.neural_types import ChannelType, LossType, MaskType, NeuralType
from nemo.utils import logging
__all__ = ["NeuralMachineTranslationModel"]
class NeuralMachineTranslationModel(ModelPT):
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"input_ids": NeuralType(('B', 'T'), ChannelType()),
"attention_mask": NeuralType(('B', 'T'), MaskType(), optional=True),
"decoder_input_ids": NeuralType(('B', 'T'), ChannelType(), optional=True),
"labels": NeuralType(('B', 'T'), ChannelType(), optional=True),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"loss": NeuralType((), LossType()),
"decoder_hidden_states": NeuralType(("B", "T", "D"), ChannelType(), optional=True),
"encoder_hidden_states": NeuralType(("B", "T", "D"), ChannelType(), optional=True),
}
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
# must assign tokenizers before init
if cfg.language_model.pretrained_model_name:
if cfg.language_model.pretrained_encoder_model_name or cfg.language_model.pretrained_decoder_model_name:
raise ValueError(
"Must have either pretrained_model_name or both pretrained_encoder_model name and "
"pretrained_decoder_model_name."
)
# setup tokenizer
self.encoder_tokenizer = self.setup_tokenizer(cfg.encoder_tokenizer)
self.encoder_add_special_tokens = cfg.encoder_tokenizer.add_special_tokens
# set decoder to encoder
self.decoder_tokenizer = self.encoder_tokenizer
self.decoder_add_special_tokens = self.encoder_add_special_tokens
else:
if not (
cfg.language_model.pretrained_encoder_model_name and cfg.language_model.pretrained_decoder_model_name
):
raise ValueError("Both encoder and decoder must be specified")
# setup tokenizers
self.encoder_tokenizer = self.setup_tokenizer(cfg.encoder_tokenizer)
self.encoder_add_special_tokens = cfg.encoder_tokenizer.add_special_tokens
self.decoder_tokenizer = self.setup_tokenizer(cfg.decoder_tokenizer)
self.decoder_add_special_tokens = cfg.decoder_tokenizer.add_special_tokens
if not self.encoder_tokenizer:
raise TypeError("encoder_tokenizer failed to initialize")
if not self.decoder_tokenizer:
raise TypeError("decoder_tokenizer failed to initialize")
# init superclass
super().__init__(cfg=cfg, trainer=trainer)
# must assign modules after init
if cfg.language_model.pretrained_model_name:
# Setup end-to-end model
if "bart" in cfg.language_model.pretrained_model_name:
self.model = BartForConditionalGeneration.from_pretrained(cfg.language_model.pretrained_model_name)
else:
self.model = AutoModel.from_pretrained(cfg.language_model.pretrained_model_name)
else:
if not (
cfg.language_model.pretrained_encoder_model_name and cfg.language_model.pretrained_decoder_model_name
):
raise ValueError("Both encoder and decoder must be specified")
# Setup encoder/decoder model
self.model = EncoderDecoderModel.from_encoder_decoder_pretrained(
encoder=cfg.language_model.pretrained_encoder_model_name,
decoder=cfg.language_model.pretrained_decoder_model_name,
)
self.validation_perplexity = Perplexity(compute_on_step=False)
self.setup_optimization(cfg.optim)
@typecheck()
def forward(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor = None,
decoder_input_ids: torch.Tensor = None,
labels: torch.Tensor = None,
) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
"""
No special modification required for Lightning, define it as you normally would
in the `nn.Module` in vanilla PyTorch.
"""
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
decoder_input_ids=decoder_input_ids,
labels=labels,
return_dict=False,
)
return outputs
@typecheck.disable_checks()
def generate(self, input_ids: torch.Tensor) -> torch.Tensor:
"""Wraps huggingface EncoderDecoder.generate()."""
outputs = self.model.generate(
input_ids=input_ids,
pad_token_id=self.encoder_tokenizer.pad_id,
bos_token_id=self.encoder_tokenizer.bos_id,
eos_token_id=self.encoder_tokenizer.eos_id,
decoder_start_token_id=self.decoder_tokenizer.bos_id,
**self._cfg.generate,
)
return outputs
def training_step(self, batch: Tuple, batch_idx: int) -> Dict:
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`. Loss calculation from HuggingFace's BartForConditionalGeneration.
"""
input_ids, input_mask, decoder_input_ids, labels = batch
loss = self.forward(
input_ids=input_ids, attention_mask=input_mask, decoder_input_ids=decoder_input_ids, labels=labels,
)[0]
tensorboard_logs = {"train_loss": loss, "lr": self._optimizer.param_groups[0]["lr"]}
return {"loss": loss, "log": tensorboard_logs}
def validation_step(self, batch: Tuple, batch_idx: int) -> Dict:
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`. Loss calculation from HuggingFace's BartForConditionalGeneration.
"""
input_ids, input_mask, decoder_input_ids, labels = batch
loss, logits = self.forward(
input_ids=input_ids, attention_mask=input_mask, decoder_input_ids=decoder_input_ids, labels=labels,
)[:2]
self.validation_perplexity(logits=logits)
tensorboard_logs = {"val_loss": loss}
return {"val_loss": loss, "log": tensorboard_logs}
def validation_epoch_end(self, outputs: List[Dict]) -> Dict:
"""
Called at the end of validation to aggregate outputs.
:param outputs: list of individual outputs of each validation step.
"""
avg_loss = torch.stack([x["val_loss"] for x in outputs]).mean()
perplexity = self.validation_perplexity.compute()
tensorboard_logs = {"val_loss": avg_loss, "perplexity": perplexity}
logging.info(f"evaluation perplexity {perplexity.item()}")
return {"val_loss": avg_loss, "log": tensorboard_logs}
@typecheck.disable_checks()
def test_step(self, batch: Tuple, batch_idx: int) -> torch.Tensor:
"""Lightning calls this inside the test loop with data from the test dataloader."""
input_ids, input_mask, decoder_input_ids, labels = batch
sequences = self.generate(input_ids=input_ids)
return sequences
@typecheck.disable_checks()
def test_epoch_end(self, outputs: List[torch.Tensor]) -> Dict[str, List[str]]:
"""Called at the end of test to aggregate outputs and decode them."""
texts = [self.encoder_tokenizer.ids_to_text(seq) for batch in outputs for seq in batch]
return {"texts": texts}
def setup_tokenizer(self, cfg: DictConfig):
tokenizer = get_tokenizer(
tokenizer_name=cfg.tokenizer_name,
tokenizer_model=cfg.tokenizer_model,
special_tokens=OmegaConf.to_container(cfg.special_tokens) if cfg.special_tokens else None,
vocab_file=cfg.vocab_file,
)
return tokenizer
def setup_training_data(self, train_data_config: Optional[DictConfig]):
self._train_dl = self.setup_dataloader_from_config(cfg=train_data_config)
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
self._validation_dl = self.setup_dataloader_from_config(cfg=val_data_config)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
self._test_dl = self.setup_dataloader_from_config(cfg=test_data_config)
def setup_dataloader_from_config(self, cfg: DictConfig):
dataset = NeuralMachineTranslationDataset(
filepath=cfg.filepath,
encoder_tokenizer=self.encoder_tokenizer,
decoder_tokenizer=self.decoder_tokenizer,
encoder_add_special_tokens=self.encoder_add_special_tokens,
decoder_add_special_tokens=self.decoder_add_special_tokens,
max_seq_length=self._cfg.max_seq_length,
num_samples=cfg.get("num_samples", -1),
convert_labels=self._cfg.convert_labels,
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=self._cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=cfg.get("num_workers", 2),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
collate_fn=dataset.collate_fn,
)
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
pass
@@ -0,0 +1,463 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import glob
import hashlib
import json
import os
import shutil
import tarfile
import tempfile
from typing import Any, Dict, Optional, Union
import torch
from megatron import mpu
from megatron.checkpointing import get_checkpoint_version, set_checkpoint_version
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from pytorch_lightning.accelerators.accelerator import Accelerator
from pytorch_lightning.utilities import rank_zero_only
from transformers import TRANSFORMERS_CACHE
from nemo.collections.common.tokenizers.huggingface.auto_tokenizer import AutoTokenizer
from nemo.collections.nlp.modules import BertModule, MegatronBertEncoder
from nemo.collections.nlp.modules.common.megatron.megatron_utils import compute_model_parallel_rank
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer
from nemo.collections.nlp.parts.nlp_overrides import NLPCheckpointConnector
from nemo.core.classes import ModelPT
from nemo.core.classes.exportable import Exportable
from nemo.utils import AppState, logging
from nemo.utils.exp_manager import configure_checkpointing
from nemo.utils.get_rank import is_global_rank_zero
__all__ = ['NLPModel']
NEMO_NLP_TMP = os.path.join(os.path.dirname(str(TRANSFORMERS_CACHE)), "nemo_nlp_tmp")
os.makedirs(NEMO_NLP_TMP, exist_ok=True)
class NLPModel(ModelPT, Exportable):
"""Base class for NLP Models.
"""
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
super().__init__(cfg, trainer)
self.set_world_size(trainer)
def register_artifact(self, config_path: str, src: str, verify_src_exists: bool = False):
""" Overrides ModelPT register_artifact default behavior. NLP models usually need artifacts that are optional."""
return super().register_artifact(config_path, src, verify_src_exists=verify_src_exists)
@rank_zero_only
def register_bert_model(self):
"""Adds encoder config to .nemo archive for Jarvis.
"""
# check if there is an encoder, warn if not
if self.bert_model is None:
raise ValueError('Instantiate self.bert_model before registering it.')
else:
# get encoder config and create source for artifact
if isinstance(self.bert_model, MegatronBertEncoder):
pretrained_model_name = self.bert_model._model_name
encoder_config_path = pretrained_model_name + '_encoder_config'
encoder_config_src = os.path.join(NEMO_NLP_TMP, encoder_config_path + '.json')
config_for_json = OmegaConf.to_container(self.bert_model.config)
with open(encoder_config_src, 'w', encoding='utf-8') as f:
f.write(json.dumps(config_for_json, indent=2, sort_keys=True) + '\n')
self.register_artifact('language_model.config_file', encoder_config_src) # for .nemo
elif isinstance(self.bert_model, BertModule):
# HuggingFace Transformer Config
pretrained_model_name = self.bert_model.name_or_path
# Some HF names have "/" in them so we replace with _
pretrained_model_name = pretrained_model_name.replace("/", "_")
encoder_config_path = pretrained_model_name + '_encoder_config'
encoder_config_src = os.path.join(NEMO_NLP_TMP, encoder_config_path + '.json')
self.bert_model.config.to_json_file(encoder_config_src) # name requested by jarvis team
self.register_artifact('language_model.config_file', encoder_config_src) # for .nemo
else:
logging.info(
f'Registering BERT model config for {self.bert_model} is not yet supported. Please override this method if needed.'
)
def setup_tokenizer(self, cfg: DictConfig):
"""Instantiates tokenizer based on config and registers tokenizer artifacts.
If model is being restored from .nemo file then the tokenizer.vocab_file will
be used (if it exists).
Otherwise, we will use the vocab file provided in the config (if it exists).
Finally, if no vocab file is given (this happens frequently when using HF),
we will attempt to extract the vocab from the tokenizer object and then register it.
Args:
cfg (DictConfig): Tokenizer config
"""
vocab_file = None
if cfg.vocab_file:
vocab_file = self.register_artifact(config_path='tokenizer.vocab_file', src=cfg.vocab_file)
tokenizer = get_tokenizer(
tokenizer_name=cfg.tokenizer_name,
vocab_file=vocab_file,
special_tokens=OmegaConf.to_container(cfg.special_tokens) if cfg.special_tokens else None,
tokenizer_model=self.register_artifact(config_path='tokenizer.tokenizer_model', src=cfg.tokenizer_model),
)
self.tokenizer = tokenizer
if vocab_file is None:
# when there is no vocab file we try to get the vocab from the tokenizer and register it
self._register_vocab_from_tokenizer(vocab_file_config_path='tokenizer.vocab_file', cfg=cfg)
@rank_zero_only
def _register_vocab_from_tokenizer(
self,
vocab_file_config_path: str = 'tokenizer.vocab_file',
vocab_dict_config_path: str = 'tokenizer_vocab_dict',
cfg: DictConfig = None,
):
"""Creates vocab file from tokenizer if vocab file is None.
Args:
vocab_file_config_path: path to the vocab_file in the config
vocab_dict_config_path: path to the vocab_dict in the config
cfg: tokenizer config
"""
if self.tokenizer is None:
raise ValueError('Instantiate self.tokenizer before registering vocab from it.')
else:
if isinstance(self.tokenizer, AutoTokenizer):
# extract vocab from tokenizer
vocab_dict = self.tokenizer.tokenizer.get_vocab()
# for fast and slow tokenizer vocabularies compatibility
vocab_dict = dict(sorted(vocab_dict.items(), key=lambda item: item[1]))
# get hash of vocab_dict to create a unique directory to write vocab_dict and vocab_file
m = hashlib.md5()
if 'tokenizer_name' in cfg:
if cfg.tokenizer_name is not None:
# different pretrained models with the same vocab will have different hash
m.update(cfg.tokenizer_name.encode())
# get string representation of vocab_dict
vocab_dict_str = json.dumps(vocab_dict, sort_keys=True).encode()
m.update(vocab_dict_str)
vocab_dict_hash = m.hexdigest()
hash_path = os.path.join(NEMO_NLP_TMP, vocab_dict_hash)
os.makedirs(hash_path, exist_ok=True)
vocab_json_src = os.path.join(hash_path, vocab_dict_config_path)
with open(vocab_json_src, 'w', encoding='utf-8') as f:
f.write(json.dumps(vocab_dict, indent=2, sort_keys=True) + '\n')
self.register_artifact(config_path=vocab_dict_config_path, src=vocab_json_src)
# create vocab file
vocab_file_src = os.path.join(hash_path, vocab_file_config_path)
with open(vocab_file_src, 'w', encoding='utf-8') as f:
for key in vocab_dict:
f.write(key + '\n')
cfg.vocab_file = vocab_file_src
self.register_artifact(config_path=vocab_file_config_path, src=vocab_file_src)
else:
logging.info(
f'Registering tokenizer vocab for {self.tokenizer} is not yet supported. Please override this method if needed.'
)
def _clip_gradients(self, optimizer, clip_val=None):
""" Override of PTL Gradient Clipping.
Enables model parallel gradient clipping from Megatron-LM.
Args:
optimizer ([type]): [description]
clip_val ([type], optional): [description]. Defaults to None.
"""
app_state = AppState()
# get clip_val from trainer if None is provided
if clip_val is None:
clip_val = float(self._trainer.gradient_clip_val)
if app_state.model_parallel_size is not None:
model = self._trainer.get_model()
parameters = model.parameters()
if mpu.model_parallel_is_initialized():
mpu.grads.clip_grad_norm(parameters=parameters, max_norm=clip_val)
else:
raise ValueError('Model parallel groups must be intialized to use model parallel gradient clipping.')
else:
return Accelerator._clip_gradients(self, optimizer, clip_val)
def setup(self, stage: str) -> None:
""" PTL hook that is called on all DDP processes. """
if stage == 'fit':
# adds self.bert_model config to .nemo file
if hasattr(self, 'bert_model') and self.bert_model is not None:
self.register_bert_model()
app_state = AppState()
if app_state.model_parallel_size is not None:
self._trainer.checkpoint_connector = NLPCheckpointConnector(self._trainer)
# # Configure checkpointing for model parallel
# if app_state.create_checkpoint_callback:
# # global rank 0 is configured by exp_manager
# if not is_global_rank_zero() and app_state.data_parallel_rank == 0:
# configure_checkpointing(
# self._trainer,
# app_state.log_dir,
# app_state.checkpoint_name,
# app_state.checkpoint_callback_params,
# )
def on_save_checkpoint(self, checkpoint: Dict[str, Any]) -> None:
""" LightningModule hook that's used to save things in addition to model weights. """
if hasattr(self, "bert_model") and isinstance(self.bert_model, MegatronBertEncoder):
checkpoint['checkpoint_version'] = get_checkpoint_version()
return None
def on_load_checkpoint(self, checkpoint: Dict[str, Any]) -> None:
""" LightningModule hook that's used to restore things saved with on_save_checkpoint."""
if hasattr(self, "bert_model") and isinstance(self.bert_model, MegatronBertEncoder):
if get_checkpoint_version():
assert (
checkpoint['checkpoint_version'] == get_checkpoint_version()
), 'checkpoint version found on_load_checkpoint different than get_checkpoint_version'
else:
set_checkpoint_version(checkpoint['checkpoint_version'])
logging.info(f"Setting Megatron checkpoint version: {checkpoint['checkpoint_version']}")
return None
# no rank check as model parallel models need to be saved on data parallel rank 0
def save_to(self, save_path: str):
"""
Saves model instance (weights and configuration) into .nemo file
You can use "restore_from" method to fully restore instance from .nemo file.
.nemo file is an archive (tar.gz) with the following:
model_config.yaml - model configuration in .yaml format. You can deserialize this into cfg argument for model's constructor
model_weights.ckpt - model checkpoint
Args:
save_path: Path to .nemo file where model instance should be saved
"""
save_path = os.path.abspath(save_path)
app_state = AppState()
if app_state.model_parallel_size is not None:
self._default_save_to(save_path)
else:
# super.save_to only runs on global rank 0
return super().save_to(save_path)
def _default_save_to(self, save_path: str):
app_state = AppState()
if app_state.model_parallel_size is not None:
# each model parallel rank creates a .nemo file
# after all .nemo files are created, each rank
# will add their checkpoint to global rank 0
base_dir = os.path.dirname(save_path) # use the directory to merge mp_rank .nemo files into one
# update save_path based on model parallel_rank
base_path = os.path.splitext(save_path)[0] # everything except the extension
mp_save_path = f'{base_path}_mp_rank_{app_state.model_parallel_rank:02d}.nemo'
if app_state.data_parallel_rank == 0:
super()._default_save_to(mp_save_path)
# barrier so that all processes have finished writing their weights before creating .nemo file
torch.distributed.barrier()
if is_global_rank_zero():
# extract all tar files
for mp_rank in range(app_state.model_parallel_size):
mp_tar_path = f'{base_path}_mp_rank_{mp_rank:02d}.nemo'
mp_tar = tarfile.open(mp_tar_path, 'r:gz')
mp_tar.extractall(path=os.path.join(base_dir, f'mp_rank_{mp_rank:02d}'))
mp_tar.close()
os.remove(mp_tar_path)
# move rank 0 .nemo extract to base_path
shutil.move(os.path.join(base_dir, 'mp_rank_00'), base_path)
# move mp_rank_00 checkpoint to mp_rank_00 directory inside base_path
os.mkdir(os.path.join(base_path, 'mp_rank_00'))
shutil.move(os.path.join(base_path, 'model_weights.ckpt'), os.path.join(base_path, 'mp_rank_00'))
# move other mp_rank checkpoints from base_dir to base_path
for mp_rank in range(1, app_state.model_parallel_size):
os.mkdir(os.path.join(base_path, f'mp_rank_{mp_rank:02d}'))
shutil.move(
os.path.join(base_dir, f'mp_rank_{mp_rank:02d}', 'model_weights.ckpt'),
os.path.join(base_path, f'mp_rank_{mp_rank:02d}'),
)
# clean up leftover directory
shutil.rmtree(os.path.join(base_dir, f'mp_rank_{mp_rank:02d}'))
# create tar file from base_path
self._make_nemo_file_from_folder(save_path, base_path)
# clean up base_path
shutil.rmtree(base_path)
elif is_global_rank_zero():
return super()._default_save_to(save_path)
else:
return
@classmethod
def restore_from(
cls,
restore_path: str,
override_config_path: Optional[Union[OmegaConf, str]] = None,
map_location: Optional[torch.device] = None,
strict: bool = True,
return_config: bool = False,
trainer: Trainer = None,
):
"""
Restores model instance (weights and configuration) from .nemo file.
Args:
restore_path: path to .nemo file from which model should be instantiated
override_config_path: path to a yaml config that will override the internal
config file or an OmegaConf / DictConfig object representing the model config.
map_location: Optional torch.device() to map the instantiated model to a device.
By default (None), it will select a GPU if available, falling back to CPU otherwise.
strict: Passed to load_state_dict. Set to True by default.
return_config: If set to true, will return just the underlying config of the restored
model as an OmegaConf DictConfig object without instantiating the model.
trainer: PyTorch Lightning trainer. Must be passed in order to use model parallel .nemo
Example:
```
model = nemo.collections.nlp.models.TokenClassificationModel.restore_from('token_classification.nemo')
assert isinstance(model, nemo.collections.nlp.models.TokenClassificationModel)
```
Returns:
An instance of type cls or its underlying config (if return_config is set).
"""
if not os.path.exists(restore_path):
raise FileNotFoundError(f"Can't find {restore_path}")
app_state = AppState()
app_state.model_restore_path = os.path.abspath(os.path.expanduser(restore_path))
# detect if we have a model parallel .nemo file
with tempfile.TemporaryDirectory() as tmpdir:
cwd = os.getcwd()
os.chdir(tmpdir)
# detect if model parallel from tarfile
tar = tarfile.open(app_state.model_restore_path, "r:gz")
names = tar.getnames()
mp_ranks = []
for name in names:
if 'mp_rank' in name:
mp_ranks.append(name)
if mp_ranks:
app_state.model_parallel_size = len(mp_ranks) // 2 # directory and file are included in getnames()
# get checkpoint version
checkpoint_version_member = None
for member in tar.getmembers():
if 'megatron_checkpoint_version.json' in member.name:
checkpoint_version_member = member
tar.extract(checkpoint_version_member, tmpdir)
with open(checkpoint_version_member.name, 'r') as f:
checkpoint_version = json.load(f).get('checkpoint_version', None)
logging.info(
(
f'Detected model parallel .nemo file: {restore_path}. '
f'Assuming megatron model parallelism with '
f'model_parallel_size: {app_state.model_parallel_size} '
f'and checkpoint version: {checkpoint_version}'
)
)
tar.close()
os.chdir(cwd)
if app_state.model_parallel_size is not None:
if not isinstance(trainer, Trainer):
raise ValueError("trainer must be a PyTorch Lightning Trainer to restore model parallel .nemo files.")
if checkpoint_version is None:
raise ValueError(
"Restoring from megatron model parallel .nemo but could not find megatron checkpoint version."
)
else:
logging.info(f"Setting megatron checkpoint version: {checkpoint_version}")
set_checkpoint_version(checkpoint_version)
app_state.world_size = trainer.num_gpus * trainer.num_nodes
if trainer.local_rank is not None:
app_state.local_rank = trainer.local_rank
else:
raise ValueError("trainer.local_rank is None. local_rank needed to restore model parallel models.")
model_parallel_rank = compute_model_parallel_rank(trainer.local_rank, app_state.model_parallel_size)
app_state.model_parallel_rank = model_parallel_rank
restored_model = cls._default_restore_from(
restore_path, override_config_path, map_location, strict, return_config
)
restored_model.set_trainer(trainer)
return restored_model
else:
return super().restore_from(restore_path, override_config_path, map_location, strict, return_config)
@rank_zero_only
def register_megatron_checkpoint_version(self):
""" Adds checkpoint version to .nemo archive """
if self.bert_model is None:
raise ValueError('Instantiate self.bert_model before registering megatron checkpoint version.')
else:
# get encoder config and create source for artifact
if isinstance(self.bert_model, MegatronBertEncoder):
checkpoint_version = get_checkpoint_version()
if checkpoint_version is None:
raise ValueError('Unable to get megatron checkpoint version.')
else:
checkpoint_version_dict = {'checkpoint_version': checkpoint_version}
checkpoint_version_path = 'megatron_checkpoint_version.json'
checkpoint_version_src = os.path.join(NEMO_NLP_TMP, checkpoint_version_path)
with open(checkpoint_version_src, 'w') as f:
f.write(json.dumps(checkpoint_version_dict))
self.register_artifact(checkpoint_version_path, checkpoint_version_src)
@staticmethod
def _unpack_nemo_file(path2file: str, out_folder: str) -> str:
return super(NLPModel, NLPModel)._unpack_nemo_file(path2file, out_folder)
@staticmethod
def _make_nemo_file_from_folder(filename, source_dir):
return super(NLPModel, NLPModel)._make_nemo_file_from_folder(filename, source_dir)
@property
def input_module(self):
return self.bert_model
@property
def output_module(self):
return self.classifier
@@ -0,0 +1,15 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.question_answering.qa_model import QAModel
@@ -0,0 +1,404 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from typing import Dict, Optional
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from torch.utils.data import DataLoader
from nemo.collections.common.losses import SpanningLoss
from nemo.collections.nlp.data import SquadDataset
from nemo.collections.nlp.data.question_answering_squad.qa_squad_processing import (
EVALUATION_MODE,
INFERENCE_MODE,
TRAINING_MODE,
)
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common import TokenClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.parts.utils_funcs import tensor2list
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import NeuralType
from nemo.utils import logging
__all__ = ['QAModel']
class QAModel(NLPModel):
"""
BERT encoder with QA head training.
"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return self.classifier.output_types
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
self.setup_tokenizer(cfg.tokenizer)
super().__init__(cfg=cfg, trainer=trainer)
self.bert_model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=OmegaConf.to_container(cfg.language_model.config) if cfg.language_model.config else None,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file)
if cfg.tokenizer is not None
else None,
)
self.classifier = TokenClassifier(
hidden_size=self.bert_model.config.hidden_size,
num_classes=cfg.token_classifier.num_classes,
num_layers=cfg.token_classifier.num_layers,
activation=cfg.token_classifier.activation,
log_softmax=cfg.token_classifier.log_softmax,
dropout=cfg.token_classifier.dropout,
use_transformer_init=cfg.token_classifier.use_transformer_init,
)
self.loss = SpanningLoss()
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask
)
logits = self.classifier(hidden_states=hidden_states)
return logits
def training_step(self, batch, batch_idx):
input_ids, input_type_ids, input_mask, unique_ids, start_positions, end_positions = batch
logits = self.forward(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
loss, _, _ = self.loss(logits=logits, start_positions=start_positions, end_positions=end_positions)
lr = self._optimizer.param_groups[0]['lr']
self.log('train_loss', loss)
self.log('lr', lr, prog_bar=True)
return {'loss': loss, 'lr': lr}
def validation_step(self, batch, batch_idx):
if self.trainer.testing:
prefix = 'test'
else:
prefix = 'val'
input_ids, input_type_ids, input_mask, unique_ids, start_positions, end_positions = batch
logits = self.forward(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
loss, start_logits, end_logits = self.loss(
logits=logits, start_positions=start_positions, end_positions=end_positions
)
tensors = {
'unique_ids': unique_ids,
'start_logits': start_logits,
'end_logits': end_logits,
}
return {f'{prefix}_loss': loss, f'{prefix}_tensors': tensors}
def test_step(self, batch, batch_idx):
return self.validation_step(batch, batch_idx)
def validation_epoch_end(self, outputs):
if self.trainer.testing:
prefix = 'test'
else:
prefix = 'val'
avg_loss = torch.stack([x[f'{prefix}_loss'] for x in outputs]).mean()
unique_ids = torch.cat([x[f'{prefix}_tensors']['unique_ids'] for x in outputs])
start_logits = torch.cat([x[f'{prefix}_tensors']['start_logits'] for x in outputs])
end_logits = torch.cat([x[f'{prefix}_tensors']['end_logits'] for x in outputs])
all_unique_ids = []
all_start_logits = []
all_end_logits = []
if torch.distributed.is_initialized():
world_size = torch.distributed.get_world_size()
for ind in range(world_size):
all_unique_ids.append(torch.empty_like(unique_ids))
all_start_logits.append(torch.empty_like(start_logits))
all_end_logits.append(torch.empty_like(end_logits))
torch.distributed.all_gather(all_unique_ids, unique_ids)
torch.distributed.all_gather(all_start_logits, start_logits)
torch.distributed.all_gather(all_end_logits, end_logits)
else:
all_unique_ids.append(unique_ids)
all_start_logits.append(start_logits)
all_end_logits.append(end_logits)
exact_match, f1, all_predictions, all_nbest = -1, -1, [], []
if not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0:
unique_ids = []
start_logits = []
end_logits = []
for u in all_unique_ids:
unique_ids.extend(tensor2list(u))
for u in all_start_logits:
start_logits.extend(tensor2list(u))
for u in all_end_logits:
end_logits.extend(tensor2list(u))
eval_dataset = self._test_dl.dataset if self.trainer.testing else self._validation_dl.dataset
exact_match, f1, all_predictions, all_nbest = eval_dataset.evaluate(
unique_ids=unique_ids,
start_logits=start_logits,
end_logits=end_logits,
n_best_size=self._cfg.dataset.n_best_size,
max_answer_length=self._cfg.dataset.max_answer_length,
version_2_with_negative=self._cfg.dataset.version_2_with_negative,
null_score_diff_threshold=self._cfg.dataset.null_score_diff_threshold,
do_lower_case=self._cfg.dataset.do_lower_case,
)
logging.info(f"{prefix} exact match {exact_match}")
logging.info(f"{prefix} f1 {f1}")
self.log(f'{prefix}_loss', avg_loss)
self.log(f'{prefix}_exact_match', exact_match)
self.log(f'{prefix}_f1', f1)
def test_epoch_end(self, outputs):
return self.validation_epoch_end(outputs)
@torch.no_grad()
def inference(
self,
file: str,
batch_size: int = 1,
num_samples: int = -1,
output_nbest_file: Optional[str] = None,
output_prediction_file: Optional[str] = None,
):
"""
Get prediction for unlabeled inference data
Args:
file: inference data
batch_size: batch size to use during inference
num_samples: number of samples to use of inference data. Default: -1 if all data should be used.
output_nbest_file: optional output file for writing out nbest list
output_prediction_file: optional output file for writing out predictions
Returns:
model predictions, model nbest list
"""
# store predictions for all queries in a single list
all_predictions = []
all_nbest = []
mode = self.training
device = 'cuda' if torch.cuda.is_available() else 'cpu'
try:
# Switch model to evaluation mode
self.eval()
self.to(device)
logging_level = logging.get_verbosity()
logging.set_verbosity(logging.WARNING)
dataloader_cfg = {
"batch_size": batch_size,
"file": file,
"shuffle": False,
"num_samples": num_samples,
'num_workers': 2,
'pin_memory': False,
'drop_last': False,
}
dataloader_cfg = OmegaConf.create(dataloader_cfg)
infer_datalayer = self._setup_dataloader_from_config(cfg=dataloader_cfg, mode=INFERENCE_MODE)
all_logits = []
all_unique_ids = []
for i, batch in enumerate(infer_datalayer):
input_ids, token_type_ids, attention_mask, unique_ids = batch
logits = self.forward(
input_ids=input_ids.to(device),
token_type_ids=token_type_ids.to(device),
attention_mask=attention_mask.to(device),
)
all_logits.append(logits)
all_unique_ids.append(unique_ids)
logits = torch.cat(all_logits)
unique_ids = tensor2list(torch.cat(all_unique_ids))
s, e = logits.split(dim=-1, split_size=1)
start_logits = tensor2list(s.squeeze())
end_logits = tensor2list(e.squeeze())
(all_predictions, all_nbest, scores_diff) = infer_datalayer.dataset.get_predictions(
unique_ids=unique_ids,
start_logits=start_logits,
end_logits=end_logits,
n_best_size=self._cfg.dataset.n_best_size,
max_answer_length=self._cfg.dataset.max_answer_length,
version_2_with_negative=self._cfg.dataset.version_2_with_negative,
null_score_diff_threshold=self._cfg.dataset.null_score_diff_threshold,
do_lower_case=self._cfg.dataset.do_lower_case,
)
with open(file, 'r') as test_file_fp:
test_data = json.load(test_file_fp)["data"]
id_to_question_mapping = {}
for title in test_data:
for par in title["paragraphs"]:
for question in par["qas"]:
id_to_question_mapping[question["id"]] = question["question"]
for question_id in all_predictions:
all_predictions[question_id] = (id_to_question_mapping[question_id], all_predictions[question_id])
if output_nbest_file is not None:
with open(output_nbest_file, "w") as writer:
writer.write(json.dumps(all_nbest, indent=4) + "\n")
if output_prediction_file is not None:
with open(output_prediction_file, "w") as writer:
writer.write(json.dumps(all_predictions, indent=4) + "\n")
finally:
# set mode back to its original value
self.train(mode=mode)
logging.set_verbosity(logging_level)
return all_predictions, all_nbest
def setup_training_data(self, train_data_config: Optional[DictConfig]):
if not train_data_config or not train_data_config.file:
logging.info(
f"Dataloader config or file_path for the train is missing, so no data loader for test is created!"
)
self._test_dl = None
return
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config, mode=TRAINING_MODE)
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
if not val_data_config or not val_data_config.file:
logging.info(
f"Dataloader config or file_path for the validation is missing, so no data loader for test is created!"
)
self._test_dl = None
return
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config, mode=EVALUATION_MODE)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
if not test_data_config or test_data_config.file is None:
logging.info(
f"Dataloader config or file_path for the test is missing, so no data loader for test is created!"
)
self._test_dl = None
return
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config, mode=EVALUATION_MODE)
def _setup_dataloader_from_config(self, cfg: DictConfig, mode: str):
dataset = SquadDataset(
tokenizer=self.tokenizer,
data_file=cfg.file,
doc_stride=self._cfg.dataset.doc_stride,
max_query_length=self._cfg.dataset.max_query_length,
max_seq_length=self._cfg.dataset.max_seq_length,
version_2_with_negative=self._cfg.dataset.version_2_with_negative,
num_samples=cfg.num_samples,
mode=mode,
use_cache=self._cfg.dataset.use_cache,
)
dl = torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
collate_fn=dataset.collate_fn,
drop_last=cfg.drop_last,
shuffle=cfg.shuffle,
num_workers=cfg.num_workers,
pin_memory=cfg.pin_memory,
)
return dl
@classmethod
def list_available_models(cls) -> Optional[PretrainedModelInfo]:
"""
This method returns a list of pre-trained model which can be instantiated directly from NVIDIA's NGC cloud.
Returns:
List of available pre-trained models.
"""
result = []
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv1.1_bertbase",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv1_1_bertbase/versions/1.0.0rc1/files/qa_squadv1.1_bertbase.nemo",
description="Question answering model finetuned from NeMo BERT Base Uncased on SQuAD v1.1 dataset which obtains an exact match (EM) score of 82.78% and an F1 score of 82.78%.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv2.0_bertbase",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv2_0_bertbase/versions/1.0.0rc1/files/qa_squadv2.0_bertbase.nemo",
description="Question answering model finetuned from NeMo BERT Base Uncased on SQuAD v2.0 dataset which obtains an exact match (EM) score of 75.04% and an F1 score of 78.08%.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv1_1_bertlarge",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv1_1_bertlarge/versions/1.0.0rc1/files/qa_squadv1.1_bertlarge.nemo",
description="Question answering model finetuned from NeMo BERT Large Uncased on SQuAD v1.1 dataset which obtains an exact match (EM) score of 85.44% and an F1 score of 92.06%.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv2.0_bertlarge",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv2_0_bertlarge/versions/1.0.0rc1/files/qa_squadv2.0_bertlarge.nemo",
description="Question answering model finetuned from NeMo BERT Large Uncased on SQuAD v2.0 dataset which obtains an exact match (EM) score of 80.22% and an F1 score of 83.05%.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv1_1_megatron_cased",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv1_1_megatron_cased/versions/1.0.0rc1/files/qa_squadv1.1_megatron_cased.nemo",
description="Question answering model finetuned from Megatron Cased on SQuAD v1.1 dataset which obtains an exact match (EM) score of 88.18% and an F1 score of 94.07%.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv2.0_megatron_cased",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv2_0_megatron_cased/versions/1.0.0rc1/files/qa_squadv2.0_megatron_cased.nemo",
description="Question answering model finetuned from Megatron Cased on SQuAD v2.0 dataset which obtains an exact match (EM) score of 84.73% and an F1 score of 87.89%.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv1.1_megatron_uncased",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv1_1_megatron_uncased/versions/1.0.0rc1/files/qa_squadv1.1_megatron_uncased.nemo",
description="Question answering model finetuned from Megatron Unased on SQuAD v1.1 dataset which obtains an exact match (EM) score of 87.61% and an F1 score of 94.00%.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="qa_squadv2.0_megatron_uncased",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/qa_squadv2_0_megatron_uncased/versions/1.0.0rc1/files/qa_squadv2.0_megatron_uncased.nemo",
description="Question answering model finetuned from Megatron Uncased on SQuAD v2.0 dataset which obtains an exact match (EM) score of 84.48% and an F1 score of 87.65%.",
)
)
return result
@@ -0,0 +1,15 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.text_classification.text_classification_model import TextClassificationModel
@@ -0,0 +1,324 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Dict, List, Optional
import torch
from omegaconf import DictConfig
from pytorch_lightning import Trainer
from nemo.collections.common.losses import CrossEntropyLoss
from nemo.collections.nlp.data.text_classification import TextClassificationDataset, calc_class_weights
from nemo.collections.nlp.metrics.classification_report import ClassificationReport
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common import SequenceClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.parts.utils_funcs import tensor2list
from nemo.core.classes.common import typecheck
from nemo.core.classes.exportable import Exportable
from nemo.core.neural_types import NeuralType
from nemo.utils import logging
__all__ = ['TextClassificationModel']
class TextClassificationModel(NLPModel, Exportable):
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return self.classifier.output_types
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
"""Initializes the BERTTextClassifier model."""
# shared params for dataset and data loaders
self.dataset_cfg = cfg.dataset
# tokenizer needs to get initialized before the super.__init__()
# as dataloaders and datasets need it to process the data
self.setup_tokenizer(cfg.tokenizer)
self.class_weights = None
super().__init__(cfg=cfg, trainer=trainer)
self.bert_model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=cfg.language_model.config,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file),
)
self.classifier = SequenceClassifier(
hidden_size=self.bert_model.config.hidden_size,
num_classes=cfg.dataset.num_classes,
num_layers=cfg.classifier_head.num_output_layers,
activation='relu',
log_softmax=False,
dropout=cfg.classifier_head.fc_dropout,
use_transformer_init=True,
idx_conditioned_on=0,
)
self.create_loss_module()
# setup to track metrics
self.classification_report = ClassificationReport(
num_classes=cfg.dataset.num_classes, mode='micro', dist_sync_on_step=True
)
# register the file containing the labels into the artifacts to get stored in the '.nemo' file later
if 'class_labels' in cfg and 'class_labels_file' in cfg.class_labels and cfg.class_labels.class_labels_file:
self.register_artifact('class_labels', cfg.class_labels.class_labels_file)
def create_loss_module(self):
# create the loss module if it is not yet created by the training data loader
if not hasattr(self, 'loss'):
if hasattr(self, 'class_weights') and self.class_weights:
# You may need to increase the number of epochs for convergence when using weighted_loss
self.loss = CrossEntropyLoss(weight=self.class_weights)
else:
self.loss = CrossEntropyLoss()
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
"""
No special modification required for Lightning, define it as you normally would
in the `nn.Module` in vanilla PyTorch.
"""
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask
)
logits = self.classifier(hidden_states=hidden_states)
return logits
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
# forward pass
input_ids, input_type_ids, input_mask, labels = batch
logits = self.forward(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
train_loss = self.loss(logits=logits, labels=labels)
lr = self._optimizer.param_groups[0]['lr']
self.log('train_loss', train_loss)
self.log('lr', lr, prog_bar=True)
return {
'loss': train_loss,
'lr': lr,
}
def validation_step(self, batch, batch_idx):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
input_ids, input_type_ids, input_mask, labels = batch
logits = self.forward(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
val_loss = self.loss(logits=logits, labels=labels)
preds = torch.argmax(logits, axis=-1)
tp, fn, fp, _ = self.classification_report(preds, labels)
return {'val_loss': val_loss, 'tp': tp, 'fn': fn, 'fp': fp}
def validation_epoch_end(self, outputs):
"""
Called at the end of validation to aggregate outputs.
:param outputs: list of individual outputs of each validation step.
"""
if not outputs:
return {}
if self.trainer.testing:
prefix = 'test'
else:
prefix = 'val'
avg_loss = torch.stack([x[f'val_loss'] for x in outputs]).mean()
# calculate metrics and classification report
precision, recall, f1, report = self.classification_report.compute()
logging.info(f'{prefix}_report: {report}')
self.log(f'{prefix}_loss', avg_loss, prog_bar=True)
self.log(f'{prefix}_precision', precision)
self.log(f'{prefix}_f1', f1)
self.log(f'{prefix}_recall', recall)
self.classification_report.reset()
def test_step(self, batch, batch_idx):
"""
Lightning calls this inside the test loop with the data from the test dataloader
passed in as `batch`.
"""
return self.validation_step(batch, batch_idx)
def test_epoch_end(self, outputs):
"""
Called at the end of test to aggregate outputs.
:param outputs: list of individual outputs of each test step.
"""
return self.validation_epoch_end(outputs)
def setup_training_data(self, train_data_config: Optional[DictConfig]):
if not train_data_config or not train_data_config.file_path:
logging.info(
f"Dataloader config or file_path for the train is missing, so no data loader for test is created!"
)
self._test_dl = None
return
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
# calculate the class weights to be used in the loss function
if self.cfg.dataset.class_balancing == 'weighted_loss':
self.class_weights = calc_class_weights(train_data_config.file_path, self.cfg.dataset.num_classes)
else:
self.class_weights = None
# we need to create/update the loss module by using the weights calculated from the training data
self.create_loss_module()
def setup_validation_data(self, val_data_config: Optional[DictConfig]):
if not val_data_config or not val_data_config.file_path:
logging.info(
f"Dataloader config or file_path for the validation is missing, so no data loader for test is created!"
)
self._test_dl = None
return
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config)
def setup_test_data(self, test_data_config: Optional[DictConfig]):
if not test_data_config or not test_data_config.file_path:
logging.info(
f"Dataloader config or file_path for the test is missing, so no data loader for test is created!"
)
self._test_dl = None
return
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config)
def _setup_dataloader_from_config(self, cfg: Dict) -> 'torch.utils.data.DataLoader':
input_file = cfg.file_path
if not os.path.exists(input_file):
raise FileNotFoundError(
f'{input_file} not found! The data should be be stored in TAB-separated files \n\
"validation_ds.file_path" and "train_ds.file_path" for train and evaluation respectively. \n\
Each line of the files contains text sequences, where words are separated with spaces. \n\
The label of the example is separated with TAB at the end of each line. \n\
Each line of the files should follow the format: \n\
[WORD][SPACE][WORD][SPACE][WORD][...][TAB][LABEL]'
)
dataset = TextClassificationDataset(
tokenizer=self.tokenizer,
input_file=input_file,
max_seq_length=self.dataset_cfg.max_seq_length,
num_samples=cfg.get("num_samples", -1),
shuffle=cfg.shuffle,
use_cache=self.dataset_cfg.use_cache,
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=cfg.get("num_workers", 0),
pin_memory=cfg.get("pin_memory", False),
drop_last=cfg.get("drop_last", False),
collate_fn=dataset.collate_fn,
)
@torch.no_grad()
def classifytext(self, queries: List[str], batch_size: int = 1, max_seq_length: int = -1) -> List[int]:
"""
Get prediction for the queries
Args:
queries: text sequences
batch_size: batch size to use during inference
max_seq_length: sequences longer than max_seq_length will get truncated. default -1 disables truncation.
Returns:
all_preds: model predictions
"""
# store predictions for all queries in a single list
all_preds = []
mode = self.training
device = next(self.parameters()).device
try:
# Switch model to evaluation mode
self.eval()
logging_level = logging.get_verbosity()
logging.set_verbosity(logging.WARNING)
dataloader_cfg = {"batch_size": batch_size, "num_workers": 3, "pin_memory": False}
infer_datalayer = self._setup_infer_dataloader(dataloader_cfg, queries, max_seq_length)
for i, batch in enumerate(infer_datalayer):
input_ids, input_type_ids, input_mask, subtokens_mask = batch
logits = self.forward(
input_ids=input_ids.to(device),
token_type_ids=input_type_ids.to(device),
attention_mask=input_mask.to(device),
)
preds = tensor2list(torch.argmax(logits, axis=-1))
all_preds.extend(preds)
finally:
# set mode back to its original value
self.train(mode=mode)
logging.set_verbosity(logging_level)
return all_preds
def _setup_infer_dataloader(
self, cfg: Dict, queries: List[str], max_seq_length: int = -1
) -> 'torch.utils.data.DataLoader':
"""
Setup function for a infer data loader.
Args:
cfg: config dictionary containing data loader params like batch_size, num_workers and pin_memory
queries: text
max_seq_length: maximum length of queries, default is -1 for no limit
Returns:
A pytorch DataLoader.
"""
dataset = TextClassificationDataset(tokenizer=self.tokenizer, queries=queries, max_seq_length=max_seq_length)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg["batch_size"],
shuffle=False,
num_workers=cfg.get("num_workers", 0),
pin_memory=cfg.get("pin_memory", False),
drop_last=False,
collate_fn=dataset.collate_fn,
)
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
pass
@classmethod
def from_pretrained(cls, name: str):
pass
@@ -0,0 +1,19 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.models.token_classification.punctuation_capitalization_model import (
PunctuationCapitalizationModel,
)
from nemo.collections.nlp.models.token_classification.token_classification_model import TokenClassificationModel
@@ -0,0 +1,490 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Dict, List, Optional
import onnx
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from nemo.collections.common.losses import AggregatorLoss, CrossEntropyLoss
from nemo.collections.nlp.data.token_classification.punctuation_capitalization_dataset import (
BertPunctuationCapitalizationDataset,
BertPunctuationCapitalizationInferDataset,
)
from nemo.collections.nlp.metrics.classification_report import ClassificationReport
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common import TokenClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.parts.utils_funcs import tensor2list
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.classes.exportable import Exportable, ExportFormat
from nemo.core.neural_types import LogitsType, NeuralType
from nemo.utils import logging
__all__ = ['PunctuationCapitalizationModel']
class PunctuationCapitalizationModel(NLPModel, Exportable):
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"punct_logits": NeuralType(('B', 'T', 'C'), LogitsType()),
"capit_logits": NeuralType(('B', 'T', 'C'), LogitsType()),
}
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
"""
Initializes BERT Punctuation and Capitalization model.
"""
self.setup_tokenizer(cfg.tokenizer)
super().__init__(cfg=cfg, trainer=trainer)
self.bert_model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=OmegaConf.to_container(cfg.language_model.config) if cfg.language_model.config else None,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file),
)
self.punct_classifier = TokenClassifier(
hidden_size=self.bert_model.config.hidden_size,
num_classes=len(self._cfg.punct_label_ids),
activation=cfg.punct_head.activation,
log_softmax=False,
dropout=cfg.punct_head.fc_dropout,
num_layers=cfg.punct_head.punct_num_fc_layers,
use_transformer_init=cfg.punct_head.use_transformer_init,
)
self.capit_classifier = TokenClassifier(
hidden_size=self.bert_model.config.hidden_size,
num_classes=len(self._cfg.capit_label_ids),
activation=cfg.capit_head.activation,
log_softmax=False,
dropout=cfg.capit_head.fc_dropout,
num_layers=cfg.capit_head.capit_num_fc_layers,
use_transformer_init=cfg.capit_head.use_transformer_init,
)
self.loss = CrossEntropyLoss(logits_ndim=3)
self.agg_loss = AggregatorLoss(num_inputs=2)
# setup to track metrics
self.punct_class_report = ClassificationReport(
num_classes=len(self._cfg.punct_label_ids),
label_ids=self._cfg.punct_label_ids,
mode='macro',
dist_sync_on_step=True,
)
self.capit_class_report = ClassificationReport(
num_classes=len(self._cfg.capit_label_ids),
label_ids=self._cfg.capit_label_ids,
mode='macro',
dist_sync_on_step=True,
)
@typecheck()
def forward(self, input_ids, attention_mask, token_type_ids=None):
"""
No special modification required for Lightning, define it as you normally would
in the `nn.Module` in vanilla PyTorch.
"""
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask
)
punct_logits = self.punct_classifier(hidden_states=hidden_states)
capit_logits = self.capit_classifier(hidden_states=hidden_states)
return punct_logits, capit_logits
def _make_step(self, batch):
input_ids, input_type_ids, input_mask, subtokens_mask, loss_mask, punct_labels, capit_labels = batch
punct_logits, capit_logits = self(
input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask
)
punct_loss = self.loss(logits=punct_logits, labels=punct_labels, loss_mask=loss_mask)
capit_loss = self.loss(logits=capit_logits, labels=capit_labels, loss_mask=loss_mask)
loss = self.agg_loss(loss_1=punct_loss, loss_2=capit_loss)
return loss, punct_logits, capit_logits
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
loss, _, _ = self._make_step(batch)
lr = self._optimizer.param_groups[0]['lr']
self.log('lr', lr, prog_bar=True)
self.log('train_loss', loss)
return {'loss': loss, 'lr': lr}
def validation_step(self, batch, batch_idx, dataloader_idx=0):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
_, _, _, subtokens_mask, _, punct_labels, capit_labels = batch
val_loss, punct_logits, capit_logits = self._make_step(batch)
subtokens_mask = subtokens_mask > 0.5
punct_preds = torch.argmax(punct_logits, axis=-1)[subtokens_mask]
punct_labels = punct_labels[subtokens_mask]
self.punct_class_report.update(punct_preds, punct_labels)
capit_preds = torch.argmax(capit_logits, axis=-1)[subtokens_mask]
capit_labels = capit_labels[subtokens_mask]
self.capit_class_report.update(capit_preds, capit_labels)
return {
'val_loss': val_loss,
'punct_tp': self.punct_class_report.tp,
'punct_fn': self.punct_class_report.fn,
'punct_fp': self.punct_class_report.fp,
'capit_tp': self.capit_class_report.tp,
'capit_fn': self.capit_class_report.fn,
'capit_fp': self.capit_class_report.fp,
}
def test_step(self, batch, batch_idx, dataloader_idx=0):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
_, _, _, subtokens_mask, _, punct_labels, capit_labels = batch
test_loss, punct_logits, capit_logits = self._make_step(batch)
subtokens_mask = subtokens_mask > 0.5
punct_preds = torch.argmax(punct_logits, axis=-1)[subtokens_mask]
punct_labels = punct_labels[subtokens_mask]
self.punct_class_report.update(punct_preds, punct_labels)
capit_preds = torch.argmax(capit_logits, axis=-1)[subtokens_mask]
capit_labels = capit_labels[subtokens_mask]
self.capit_class_report.update(capit_preds, capit_labels)
return {
'test_loss': test_loss,
'punct_tp': self.punct_class_report.tp,
'punct_fn': self.punct_class_report.fn,
'punct_fp': self.punct_class_report.fp,
'capit_tp': self.capit_class_report.tp,
'capit_fn': self.capit_class_report.fn,
'capit_fp': self.capit_class_report.fp,
}
def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0):
"""
Called at the end of validation to aggregate outputs.
outputs: list of individual outputs of each validation step.
"""
avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
# calculate metrics and log classification report for Punctuation task
punct_precision, punct_recall, punct_f1, punct_report = self.punct_class_report.compute()
logging.info(f'Punctuation report: {punct_report}')
# calculate metrics and log classification report for Capitalization task
capit_precision, capit_recall, capit_f1, capit_report = self.capit_class_report.compute()
logging.info(f'Capitalization report: {capit_report}')
self.log('val_loss', avg_loss, prog_bar=True)
self.log('punct_precision', punct_precision)
self.log('punct_f1', punct_f1)
self.log('punct_recall', punct_recall)
self.log('capit_precision', capit_precision)
self.log('capit_f1', capit_f1)
self.log('capit_recall', capit_recall)
self.punct_class_report.reset()
self.capit_class_report.reset()
def multi_test_epoch_end(self, outputs, dataloader_idx: int = 0):
"""
Called at the end of test to aggregate outputs.
outputs: list of individual outputs of each validation step.
"""
avg_loss = torch.stack([x['test_loss'] for x in outputs]).mean()
# calculate metrics and log classification report for Punctuation task
punct_precision, punct_recall, punct_f1, punct_report = self.punct_class_report.compute()
logging.info(f'Punctuation report: {punct_report}')
# calculate metrics and log classification report for Capitalization task
capit_precision, capit_recall, capit_f1, capit_report = self.capit_class_report.compute()
logging.info(f'Capitalization report: {capit_report}')
self.log('test_loss', avg_loss, prog_bar=True)
self.log('punct_precision', punct_precision)
self.log('punct_f1', punct_f1)
self.log('punct_recall', punct_recall)
self.log('capit_precision', capit_precision)
self.log('capit_f1', capit_f1)
self.log('capit_recall', capit_recall)
def update_data_dir(self, data_dir: str) -> None:
"""
Update data directory
Args:
data_dir: path to data directory
"""
if os.path.exists(data_dir):
logging.info(f'Setting model.dataset.data_dir to {data_dir}.')
self._cfg.dataset.data_dir = data_dir
else:
raise ValueError(f'{data_dir} not found')
def setup_training_data(self, train_data_config: Optional[DictConfig] = None):
"""Setup training data"""
if train_data_config is None:
train_data_config = self._cfg.train_ds
# for older(pre - 1.0.0.b3) configs compatibility
if not hasattr(self._cfg, "class_labels") or self._cfg.class_labels is None:
OmegaConf.set_struct(self._cfg, False)
self._cfg.class_labels = {}
self._cfg.class_labels = OmegaConf.create(
{'punct_labels_file': 'punct_label_ids.csv', 'capit_labels_file': 'capit_label_ids.csv'}
)
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
if not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0:
self.register_artifact('class_labels.punct_labels_file', self._train_dl.dataset.punct_label_ids_file)
self.register_artifact('class_labels.capit_labels_file', self._train_dl.dataset.capit_label_ids_file)
# save label maps to the config
self._cfg.punct_label_ids = OmegaConf.create(self._train_dl.dataset.punct_label_ids)
self._cfg.capit_label_ids = OmegaConf.create(self._train_dl.dataset.capit_label_ids)
def setup_validation_data(self, val_data_config: Optional[Dict] = None):
"""
Setup validaton data
val_data_config: validation data config
"""
if val_data_config is None:
val_data_config = self._cfg.validation_ds
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config)
def setup_test_data(self, test_data_config: Optional[Dict] = None):
if test_data_config is None:
test_data_config = self._cfg.test_ds
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config)
def _setup_dataloader_from_config(self, cfg: DictConfig):
# use data_dir specified in the ds_item to run evaluation on multiple datasets
if 'ds_item' in cfg and cfg.ds_item is not None:
data_dir = cfg.ds_item
else:
data_dir = self._cfg.dataset.data_dir
text_file = os.path.join(data_dir, cfg.text_file)
label_file = os.path.join(data_dir, cfg.labels_file)
dataset = BertPunctuationCapitalizationDataset(
tokenizer=self.tokenizer,
text_file=text_file,
label_file=label_file,
pad_label=self._cfg.dataset.pad_label,
punct_label_ids=self._cfg.punct_label_ids,
capit_label_ids=self._cfg.capit_label_ids,
max_seq_length=self._cfg.dataset.max_seq_length,
ignore_extra_tokens=self._cfg.dataset.ignore_extra_tokens,
ignore_start_end=self._cfg.dataset.ignore_start_end,
use_cache=self._cfg.dataset.use_cache,
num_samples=cfg.num_samples,
punct_label_ids_file=self._cfg.class_labels.punct_labels_file
if 'class_labels' in self._cfg
else 'punct_label_ids.csv',
capit_label_ids_file=self._cfg.class_labels.capit_labels_file
if 'class_labels' in self._cfg
else 'capit_label_ids.csv',
)
return torch.utils.data.DataLoader(
dataset=dataset,
collate_fn=dataset.collate_fn,
batch_size=cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=self._cfg.dataset.num_workers,
pin_memory=self._cfg.dataset.pin_memory,
drop_last=self._cfg.dataset.drop_last,
)
def _setup_infer_dataloader(
self, queries: List[str], batch_size: int, max_seq_length: int = None
) -> 'torch.utils.data.DataLoader':
"""
Setup function for a infer data loader.
Args:
queries: lower cased text without punctuation
batch_size: batch size to use during inference
max_seq_length: maximum sequence length after tokenization
Returns:
A pytorch DataLoader.
"""
if max_seq_length is None:
max_seq_length = self._cfg.dataset.max_seq_length
dataset = BertPunctuationCapitalizationInferDataset(
tokenizer=self.tokenizer, queries=queries, max_seq_length=max_seq_length
)
return torch.utils.data.DataLoader(
dataset=dataset,
collate_fn=dataset.collate_fn,
batch_size=batch_size,
shuffle=False,
num_workers=self._cfg.dataset.num_workers,
pin_memory=self._cfg.dataset.pin_memory,
drop_last=False,
)
def add_punctuation_capitalization(
self, queries: List[str], batch_size: int = None, max_seq_length: int = 512
) -> List[str]:
"""
Adds punctuation and capitalization to the queries. Use this method for debugging and prototyping.
Args:
queries: lower cased text without punctuation
batch_size: batch size to use during inference
max_seq_length: maximum sequence length after tokenization
Returns:
result: text with added capitalization and punctuation
"""
if queries is None or len(queries) == 0:
return []
if batch_size is None:
batch_size = len(queries)
logging.info(f'Using batch size {batch_size} for inference')
# We will store the output here
result = []
# Model's mode and device
mode = self.training
device = 'cuda' if torch.cuda.is_available() else 'cpu'
try:
# Switch model to evaluation mode
self.eval()
self = self.to(device)
infer_datalayer = self._setup_infer_dataloader(queries, batch_size, max_seq_length)
# store predictions for all queries in a single list
all_punct_preds = []
all_capit_preds = []
for batch in infer_datalayer:
input_ids, input_type_ids, input_mask, subtokens_mask = batch
punct_logits, capit_logits = self.forward(
input_ids=input_ids.to(device),
token_type_ids=input_type_ids.to(device),
attention_mask=input_mask.to(device),
)
subtokens_mask = subtokens_mask > 0.5
punct_preds = [
tensor2list(p_l[subtokens_mask[i]]) for i, p_l in enumerate(torch.argmax(punct_logits, axis=-1))
]
capit_preds = [
tensor2list(c_l[subtokens_mask[i]]) for i, c_l in enumerate(torch.argmax(capit_logits, axis=-1))
]
all_punct_preds.extend(punct_preds)
all_capit_preds.extend(capit_preds)
punct_ids_to_labels = {v: k for k, v in self._cfg.punct_label_ids.items()}
capit_ids_to_labels = {v: k for k, v in self._cfg.capit_label_ids.items()}
queries = [q.strip().split() for q in queries]
for i, query in enumerate(queries):
punct_preds = all_punct_preds[i]
capit_preds = all_capit_preds[i]
if len(query) != len(punct_preds):
logging.warning(
f'Max sequence length of query {query} is set to {max_seq_length}. Truncating the input.'
)
# removing the end of phrase punctuation of the truncated segment
punct_preds[-1] = 0
max_len = len(punct_preds)
query = query[:max_len]
query_with_punct_and_capit = ''
for j, word in enumerate(query):
punct_label = punct_ids_to_labels[punct_preds[j]]
capit_label = capit_ids_to_labels[capit_preds[j]]
if capit_label != self._cfg.dataset.pad_label:
word = word.capitalize()
query_with_punct_and_capit += word
if punct_label != self._cfg.dataset.pad_label:
query_with_punct_and_capit += punct_label
query_with_punct_and_capit += ' '
result.append(query_with_punct_and_capit.strip())
finally:
# set mode back to its original value
self.train(mode=mode)
return result
@classmethod
def list_available_models(cls) -> Optional[Dict[str, str]]:
"""
This method returns a list of pre-trained model which can be instantiated directly from NVIDIA's NGC cloud.
Returns:
List of available pre-trained models.
"""
result = []
result.append(
PretrainedModelInfo(
pretrained_model_name="punctuation_en_bert",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/punctuation_en_bert/versions/1.0.0rc1/files/punctuation_en_bert.nemo",
description="The model was trained with NeMo BERT base uncased checkpoint on a subset of data from the following sources: Tatoeba sentences, books from Project Gutenberg, Fisher transcripts.",
)
)
result.append(
PretrainedModelInfo(
pretrained_model_name="punctuation_en_distilbert",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/punctuation_en_distilbert/versions/1.0.0rc1/files/punctuation_en_distilbert.nemo",
description="The model was trained with DiltilBERT base uncased checkpoint from HuggingFace on a subset of data from the following sources: Tatoeba sentences, books from Project Gutenberg, Fisher transcripts.",
)
)
return result
@property
def input_module(self):
return self.bert_model
@property
def output_module(self):
return self
@@ -0,0 +1,522 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import Dict, List, Optional, Union
import onnx
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from torch.utils.data import DataLoader
from nemo.collections.common.losses import CrossEntropyLoss
from nemo.collections.nlp.data.data_utils.data_preprocessing import get_labels_to_labels_id_mapping
from nemo.collections.nlp.data.token_classification.token_classification_dataset import (
BertTokenClassificationDataset,
BertTokenClassificationInferDataset,
)
from nemo.collections.nlp.data.token_classification.token_classification_utils import get_label_ids
from nemo.collections.nlp.metrics.classification_report import ClassificationReport
from nemo.collections.nlp.models.nlp_model import NLPModel
from nemo.collections.nlp.modules.common import TokenClassifier
from nemo.collections.nlp.modules.common.lm_utils import get_lm_model
from nemo.collections.nlp.parts.utils_funcs import get_classification_report, plot_confusion_matrix, tensor2list
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import NeuralType
from nemo.utils import logging
__all__ = ['TokenClassificationModel']
class TokenClassificationModel(NLPModel):
"""Token Classification Model with BERT, applicable for tasks such as Named Entity Recognition"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return self.bert_model.input_types
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return self.classifier.output_types
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
"""Initializes Token Classification Model."""
# extract str to int labels mapping if a mapping file provided
if isinstance(cfg.label_ids, str):
if os.path.exists(cfg.label_ids):
logging.info(f'Reusing label_ids file found at {cfg.label_ids}.')
label_ids = get_labels_to_labels_id_mapping(cfg.label_ids)
# update the config to store name to id mapping
cfg.label_ids = OmegaConf.create(label_ids)
else:
raise ValueError(f'{cfg.label_ids} not found.')
self.setup_tokenizer(cfg.tokenizer)
self.class_weights = None
super().__init__(cfg=cfg, trainer=trainer)
self.bert_model = get_lm_model(
pretrained_model_name=cfg.language_model.pretrained_model_name,
config_file=self.register_artifact('language_model.config_file', cfg.language_model.config_file),
config_dict=OmegaConf.to_container(cfg.language_model.config) if cfg.language_model.config else None,
checkpoint_file=cfg.language_model.lm_checkpoint,
vocab_file=self.register_artifact('tokenizer.vocab_file', cfg.tokenizer.vocab_file),
)
self.classifier = TokenClassifier(
hidden_size=self.bert_model.config.hidden_size,
num_classes=len(self._cfg.label_ids),
num_layers=self._cfg.head.num_fc_layers,
activation=self._cfg.head.activation,
log_softmax=False,
dropout=self._cfg.head.fc_dropout,
use_transformer_init=self._cfg.head.use_transformer_init,
)
self.loss = self.setup_loss(class_balancing=self._cfg.dataset.class_balancing)
# setup to track metrics
self.classification_report = ClassificationReport(
len(self._cfg.label_ids), label_ids=self._cfg.label_ids, dist_sync_on_step=True
)
def update_data_dir(self, data_dir: str) -> None:
"""
Update data directory and get data stats with Data Descriptor
Weights are later used to setup loss
Args:
data_dir: path to data directory
"""
self._cfg.dataset.data_dir = data_dir
logging.info(f'Setting model.dataset.data_dir to {data_dir}.')
def setup_loss(self, class_balancing: str = None):
"""Setup loss
Setup or update loss.
Args:
class_balancing: whether to use class weights during training
"""
if class_balancing not in ['weighted_loss', None]:
raise ValueError(f'Class balancing {class_balancing} is not supported. Choose from: [null, weighted_loss]')
if class_balancing == 'weighted_loss' and self.class_weights:
# you may need to increase the number of epochs for convergence when using weighted_loss
loss = CrossEntropyLoss(logits_ndim=3, weight=self.class_weights)
logging.debug(f'Using {class_balancing} class balancing.')
else:
loss = CrossEntropyLoss(logits_ndim=3)
logging.debug(f'Using CrossEntropyLoss class balancing.')
return loss
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
hidden_states = self.bert_model(
input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask
)
logits = self.classifier(hidden_states=hidden_states)
return logits
def training_step(self, batch, batch_idx):
"""
Lightning calls this inside the training loop with the data from the training dataloader
passed in as `batch`.
"""
input_ids, input_type_ids, input_mask, subtokens_mask, loss_mask, labels = batch
logits = self(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
loss = self.loss(logits=logits, labels=labels, loss_mask=loss_mask)
lr = self._optimizer.param_groups[0]['lr']
self.log('train_loss', loss)
self.log('lr', lr, prog_bar=True)
return {
'loss': loss,
'lr': lr,
}
def validation_step(self, batch, batch_idx):
"""
Lightning calls this inside the validation loop with the data from the validation dataloader
passed in as `batch`.
"""
input_ids, input_type_ids, input_mask, subtokens_mask, loss_mask, labels = batch
logits = self(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
val_loss = self.loss(logits=logits, labels=labels, loss_mask=loss_mask)
subtokens_mask = subtokens_mask > 0.5
preds = torch.argmax(logits, axis=-1)[subtokens_mask]
labels = labels[subtokens_mask]
tp, fn, fp, _ = self.classification_report(preds, labels)
return {'val_loss': val_loss, 'tp': tp, 'fn': fn, 'fp': fp}
def validation_epoch_end(self, outputs):
"""
Called at the end of validation to aggregate outputs.
outputs: list of individual outputs of each validation step.
"""
avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
# calculate metrics and classification report
precision, recall, f1, report = self.classification_report.compute()
logging.info(report)
self.log('val_loss', avg_loss, prog_bar=True)
self.log('precision', precision)
self.log('f1', f1)
self.log('recall', recall)
self.classification_report.reset()
def test_step(self, batch, batch_idx):
input_ids, input_type_ids, input_mask, subtokens_mask, loss_mask, labels = batch
logits = self(input_ids=input_ids, token_type_ids=input_type_ids, attention_mask=input_mask)
val_loss = self.loss(logits=logits, labels=labels, loss_mask=loss_mask)
subtokens_mask = subtokens_mask > 0.5
preds = torch.argmax(logits, axis=-1)[subtokens_mask]
labels = labels[subtokens_mask]
tp, fn, fp, _ = self.classification_report(preds, labels)
return {'test_loss': val_loss, 'tp': tp, 'fn': fn, 'fp': fp}
def test_epoch_end(self, outputs):
avg_loss = torch.stack([x['test_loss'] for x in outputs]).mean()
# calculate metrics and classification report
precision, recall, f1, report = self.classification_report.compute()
logging.info(report)
self.log('test_loss', avg_loss, prog_bar=True)
self.log('precision', precision)
self.log('f1', f1)
self.log('recall', recall)
def setup_training_data(self, train_data_config: Optional[DictConfig] = None):
if train_data_config is None:
train_data_config = self._cfg.train_ds
labels_file = os.path.join(self._cfg.dataset.data_dir, train_data_config.labels_file)
# for older(pre - 1.0.0.b3) configs compatibility
if not hasattr(self._cfg, "class_labels") or self._cfg.class_labels is None:
OmegaConf.set_struct(self._cfg, False)
self._cfg.class_labels = {}
self._cfg.class_labels = OmegaConf.create({'class_labels_file': 'label_ids.csv'})
OmegaConf.set_struct(self._cfg, True)
label_ids, label_ids_filename, self.class_weights = get_label_ids(
label_file=labels_file,
is_training=True,
pad_label=self._cfg.dataset.pad_label,
label_ids_dict=self._cfg.label_ids,
get_weights=True,
class_labels_file_artifact=self._cfg.class_labels.class_labels_file,
)
# save label maps to the config
self._cfg.label_ids = OmegaConf.create(label_ids)
self.register_artifact('class_labels.class_labels_file', label_ids_filename)
self._train_dl = self._setup_dataloader_from_config(cfg=train_data_config)
def setup_validation_data(self, val_data_config: Optional[DictConfig] = None):
if val_data_config is None:
val_data_config = self._cfg.validation_ds
labels_file = os.path.join(self._cfg.dataset.data_dir, val_data_config.labels_file)
get_label_ids(
label_file=labels_file,
is_training=False,
pad_label=self._cfg.dataset.pad_label,
label_ids_dict=self._cfg.label_ids,
get_weights=False,
)
self._validation_dl = self._setup_dataloader_from_config(cfg=val_data_config)
def setup_test_data(self, test_data_config: Optional[DictConfig] = None):
if test_data_config is None:
test_data_config = self._cfg.test_ds
labels_file = os.path.join(self._cfg.dataset.data_dir, test_data_config.labels_file)
get_label_ids(
label_file=labels_file,
is_training=False,
pad_label=self._cfg.dataset.pad_label,
label_ids_dict=self._cfg.label_ids,
get_weights=False,
)
self._test_dl = self._setup_dataloader_from_config(cfg=test_data_config)
def _setup_dataloader_from_config(self, cfg: DictConfig) -> DataLoader:
"""
Setup dataloader from config
Args:
cfg: config for the dataloader
Return:
Pytorch Dataloader
"""
dataset_cfg = self._cfg.dataset
data_dir = dataset_cfg.data_dir
if not os.path.exists(data_dir):
raise FileNotFoundError(f"Data directory is not found at: {data_dir}.")
text_file = os.path.join(data_dir, cfg.text_file)
labels_file = os.path.join(data_dir, cfg.labels_file)
if not (os.path.exists(text_file) and os.path.exists(labels_file)):
raise FileNotFoundError(
f'{text_file} or {labels_file} not found. The data should be split into 2 files: text.txt and \
labels.txt. Each line of the text.txt file contains text sequences, where words are separated with \
spaces. The labels.txt file contains corresponding labels for each word in text.txt, the labels are \
separated with spaces. Each line of the files should follow the format: \
[WORD] [SPACE] [WORD] [SPACE] [WORD] (for text.txt) and \
[LABEL] [SPACE] [LABEL] [SPACE] [LABEL] (for labels.txt).'
)
dataset = BertTokenClassificationDataset(
text_file=text_file,
label_file=labels_file,
max_seq_length=dataset_cfg.max_seq_length,
tokenizer=self.tokenizer,
num_samples=cfg.num_samples,
pad_label=dataset_cfg.pad_label,
label_ids=self._cfg.label_ids,
ignore_extra_tokens=dataset_cfg.ignore_extra_tokens,
ignore_start_end=dataset_cfg.ignore_start_end,
use_cache=dataset_cfg.use_cache,
)
return DataLoader(
dataset=dataset,
collate_fn=dataset.collate_fn,
batch_size=cfg.batch_size,
shuffle=cfg.shuffle,
num_workers=dataset_cfg.num_workers,
pin_memory=dataset_cfg.pin_memory,
drop_last=dataset_cfg.drop_last,
)
def _setup_infer_dataloader(self, queries: List[str], batch_size: int) -> 'torch.utils.data.DataLoader':
"""
Setup function for a infer data loader.
Args:
queries: text
batch_size: batch size to use during inference
Returns:
A pytorch DataLoader.
"""
dataset = BertTokenClassificationInferDataset(tokenizer=self.tokenizer, queries=queries, max_seq_length=-1)
return torch.utils.data.DataLoader(
dataset=dataset,
collate_fn=dataset.collate_fn,
batch_size=batch_size,
shuffle=False,
num_workers=self._cfg.dataset.num_workers,
pin_memory=self._cfg.dataset.pin_memory,
drop_last=False,
)
@torch.no_grad()
def _infer(self, queries: List[str], batch_size: int = None) -> List[int]:
"""
Get prediction for the queries
Args:
queries: text sequences
batch_size: batch size to use during inference.
Returns:
all_preds: model predictions
"""
# store predictions for all queries in a single list
all_preds = []
mode = self.training
try:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Switch model to evaluation mode
self.eval()
self.to(device)
infer_datalayer = self._setup_infer_dataloader(queries, batch_size)
for batch in infer_datalayer:
input_ids, input_type_ids, input_mask, subtokens_mask = batch
logits = self.forward(
input_ids=input_ids.to(device),
token_type_ids=input_type_ids.to(device),
attention_mask=input_mask.to(device),
)
subtokens_mask = subtokens_mask > 0.5
preds = tensor2list(torch.argmax(logits, axis=-1)[subtokens_mask])
all_preds.extend(preds)
finally:
# set mode back to its original value
self.train(mode=mode)
return all_preds
def add_predictions(
self, queries: Union[List[str], str], batch_size: int = 32, output_file: Optional[str] = None
) -> List[str]:
"""
Add predicted token labels to the queries. Use this method for debugging and prototyping.
Args:
queries: text
batch_size: batch size to use during inference.
output_file: file to save models predictions
Returns:
result: text with added entities
"""
if queries is None or len(queries) == 0:
return []
if isinstance(queries, str):
logging.info(f'Reading from {queries} file')
with open(queries, 'r') as f:
queries = f.readlines()
result = []
all_preds = self._infer(queries, batch_size)
queries = [q.strip().split() for q in queries]
num_words = [len(q) for q in queries]
if sum(num_words) != len(all_preds):
raise ValueError('Pred and words must have the same length')
ids_to_labels = {v: k for k, v in self._cfg.label_ids.items()}
start_idx = 0
end_idx = 0
for query in queries:
end_idx += len(query)
# extract predictions for the current query from the list of all predictions
preds = all_preds[start_idx:end_idx]
start_idx = end_idx
query_with_entities = ''
for j, word in enumerate(query):
# strip out the punctuation to attach the entity tag to the word not to a punctuation mark
# that follows the word
if word[-1].isalpha():
punct = ''
else:
punct = word[-1]
word = word[:-1]
query_with_entities += word
label = ids_to_labels[preds[j]]
if label != self._cfg.dataset.pad_label:
query_with_entities += '[' + label + ']'
query_with_entities += punct + ' '
result.append(query_with_entities.strip())
if output_file is not None:
with open(output_file, 'w') as f:
for r in result:
f.write(r + '\n')
logging.info(f'Predictions saved to {output_file}')
return result
def evaluate_from_file(
self,
output_dir: str,
text_file: str,
labels_file: Optional[str] = None,
add_confusion_matrix: Optional[bool] = False,
normalize_confusion_matrix: Optional[bool] = True,
batch_size: int = 1,
) -> None:
"""
Run inference on data from a file, plot confusion matrix and calculate classification report.
Use this method for final evaluation.
Args:
output_dir: path to output directory to store model predictions, confusion matrix plot (if set to True)
text_file: path to file with text. Each line of the text.txt file contains text sequences, where words
are separated with spaces: [WORD] [SPACE] [WORD] [SPACE] [WORD]
labels_file (Optional): path to file with labels. Each line of the labels_file should contain
labels corresponding to each word in the text_file, the labels are separated with spaces:
[LABEL] [SPACE] [LABEL] [SPACE] [LABEL] (for labels.txt).'
add_confusion_matrix: whether to generate confusion matrix
normalize_confusion_matrix: whether to normalize confusion matrix
batch_size: batch size to use during inference.
"""
output_dir = os.path.abspath(output_dir)
with open(text_file, 'r') as f:
queries = f.readlines()
all_preds = self._infer(queries, batch_size)
with_labels = labels_file is not None
if with_labels:
with open(labels_file, 'r') as f:
all_labels_str = f.readlines()
all_labels_str = ' '.join([labels.strip() for labels in all_labels_str])
# writing labels and predictions to a file in output_dir is specified in the config
os.makedirs(output_dir, exist_ok=True)
filename = os.path.join(output_dir, 'infer_' + os.path.basename(text_file))
try:
with open(filename, 'w') as f:
if with_labels:
f.write('labels\t' + all_labels_str + '\n')
logging.info(f'Labels save to {filename}')
# convert labels from string label to ids
ids_to_labels = {v: k for k, v in self._cfg.label_ids.items()}
all_preds_str = [ids_to_labels[pred] for pred in all_preds]
f.write('preds\t' + ' '.join(all_preds_str) + '\n')
logging.info(f'Predictions saved to {filename}')
if with_labels and add_confusion_matrix:
all_labels = all_labels_str.split()
# convert labels from string label to ids
label_ids = self._cfg.label_ids
all_labels = [label_ids[label] for label in all_labels]
plot_confusion_matrix(
all_labels, all_preds, output_dir, label_ids=label_ids, normalize=normalize_confusion_matrix
)
logging.info(get_classification_report(all_labels, all_preds, label_ids))
except Exception:
logging.error(
f'When providing a file with labels, check that all labels in {labels_file} were'
f'seen during training.'
)
raise
@classmethod
def list_available_models(cls) -> Optional[PretrainedModelInfo]:
"""
This method returns a list of pre-trained model which can be instantiated directly from NVIDIA's NGC cloud.
Returns:
List of available pre-trained models.
"""
result = []
model = PretrainedModelInfo(
pretrained_model_name="ner_en_bert",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/ner_en_bert/versions/1.0.0rc1/files/ner_en_bert.nemo",
description="The model was trained on GMB (Groningen Meaning Bank) corpus for entity recognition and achieves 74.61 F1 Macro score.",
)
result.append(model)
return result
@@ -0,0 +1,33 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.modules.common import (
AlbertEncoder,
BertEncoder,
BertModule,
DistilBertEncoder,
MegatronBertEncoder,
RobertaEncoder,
SequenceClassifier,
SequenceRegression,
SequenceTokenClassifier,
get_lm_model,
get_megatron_lm_models_list,
get_pretrained_lm_models_list,
get_tokenizer,
get_tokenizer_list,
)
from nemo.collections.nlp.modules.dialogue_state_tracking.sgd_decoder import SGDDecoder
from nemo.collections.nlp.modules.dialogue_state_tracking.sgd_encoder import SGDEncoder
@@ -0,0 +1,34 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.modules.common.bert_module import BertModule
from nemo.collections.nlp.modules.common.huggingface import (
AlbertEncoder,
BertEncoder,
DistilBertEncoder,
RobertaEncoder,
)
from nemo.collections.nlp.modules.common.lm_utils import (
get_lm_model,
get_megatron_lm_models_list,
get_pretrained_lm_models_list,
)
from nemo.collections.nlp.modules.common.megatron import MegatronBertEncoder
from nemo.collections.nlp.modules.common.sequence_classifier import SequenceClassifier
from nemo.collections.nlp.modules.common.sequence_regression import SequenceRegression
from nemo.collections.nlp.modules.common.sequence_token_classifier import SequenceTokenClassifier
from nemo.collections.nlp.modules.common.token_classifier import BertPretrainingTokenClassifier, TokenClassifier
from nemo.collections.nlp.modules.common.tokenizer_utils import get_tokenizer, get_tokenizer_list
@@ -0,0 +1,93 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
from typing import Dict, Optional
import torch
from nemo.core.classes import NeuralModule
from nemo.core.classes.exportable import Exportable
from nemo.core.neural_types import ChannelType, MaskType, NeuralType
from nemo.utils import logging
__all__ = ['BertModule']
class BertModule(NeuralModule, Exportable):
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"input_ids": NeuralType(('B', 'T'), ChannelType()),
"attention_mask": NeuralType(('B', 'T'), MaskType(), optional=True),
"token_type_ids": NeuralType(('B', 'T'), ChannelType(), optional=True),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"last_hidden_states": NeuralType(('B', 'T', 'D'), ChannelType())}
@classmethod
def restore_from(cls, restore_path: str):
"""Restores module/model with weights"""
pass
@classmethod
def save_to(self, save_path: str):
"""Saves module/model with weights"""
pass
def restore_weights(self, restore_path: str):
"""Restores module/model's weights"""
logging.info(f"Restoring weights from {restore_path}")
if not os.path.exists(restore_path):
logging.warning(f'Path {restore_path} not found')
return
pretrained_dict = torch.load(restore_path)
# backward compatibility with NeMo0.11
if "state_dict" in pretrained_dict.keys():
pretrained_dict = pretrained_dict["state_dict"]
# remove prefix from pretrained dict
m = re.match("^bert.*?\.", list(pretrained_dict.keys())[0])
if m:
prefix = m.group(0)
pretrained_dict = {k[len(prefix) :]: v for k, v in pretrained_dict.items()}
model_dict = self.state_dict()
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict}
# starting with transformers 3.1.0, embeddings.position_ids is added to the model's state dict and could be
# missing in checkpoints trained with older transformers version
if 'embeddings.position_ids' in model_dict and 'embeddings.position_ids' not in pretrained_dict:
pretrained_dict['embeddings.position_ids'] = model_dict['embeddings.position_ids']
assert len(pretrained_dict) == len(model_dict)
model_dict.update(pretrained_dict)
self.load_state_dict(model_dict)
logging.info(f"Weights for {type(self).__name__} restored from {restore_path}")
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
sample = next(self.parameters())
input_ids = torch.randint(low=0, high=2048, size=(2, 16), device=sample.device)
attention_mask = torch.randint(low=0, high=1, size=(2, 16), device=sample.device)
return tuple([input_ids, attention_mask, attention_mask])
@@ -0,0 +1,87 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Optional
import torch
from torch import nn as nn
from nemo.collections.common.parts import transformer_weights_init
from nemo.core.classes import Exportable, NeuralModule, typecheck
from nemo.core.neural_types import ChannelType, NeuralType
__all__ = ['Classifier']
class Classifier(NeuralModule, Exportable):
"""
A baseclass for modules to perform various classification tasks.
"""
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
"""
Returns definitions of module input ports.
We implement it here since all NLP classifiers have the same inputs
"""
return {"hidden_states": NeuralType(('B', 'T', 'D'), ChannelType())}
def __init__(self, hidden_size: int, dropout: float = 0.0,) -> None:
"""
Initializes the Classifier base module.
Args:
hidden_size: the size of the hidden dimension
dropout: dropout to apply to the input hidden states
"""
super().__init__()
self._hidden_size = hidden_size
self.dropout = nn.Dropout(dropout)
def post_init(self, use_transformer_init: bool):
"""
Common post-processing to be called at the end of concrete Classifiers init methods
Args:
use_transformer_init : whether or not to apply transformer_weights_init
"""
if use_transformer_init:
self.apply(lambda module: transformer_weights_init(module, xavier=False))
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
bs = 8
seq = 64
sample = next(self.parameters())
input_example = torch.randn(bs, seq, self._hidden_size).to(sample.device).to(sample.dtype)
return tuple([input_example])
def save_to(self, save_path: str):
"""
Saves the module to the specified path.
Args:
save_path: Path to where to save the module.
"""
pass
@classmethod
def restore_from(cls, restore_path: str):
"""
Restores the module from the specified path.
Args:
restore_path: Path to restore the module from.
"""
pass
@@ -0,0 +1,58 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from abc import ABC
from typing import Any, Dict, Optional
from nemo.core.classes import NeuralModule
from nemo.core.neural_types import ChannelType, MaskType, NeuralType
__all__ = ['DecoderModule']
class DecoderModule(NeuralModule, ABC):
""" Base class for decoder neural module to be used in NLP models. """
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"input_ids": NeuralType(('B', 'T'), ChannelType()),
"decoder_mask": NeuralType(('B', 'T'), MaskType(), optional=True),
"encoder_mask": NeuralType(('B', 'T'), MaskType(), optional=True),
"encoder_embeddings": NeuralType(('B', 'T', 'D'), ChannelType(), optional=True),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"last_hidden_states": NeuralType(('B', 'T', 'D'), ChannelType())}
@property
def hidden_size(self) -> Optional[int]:
raise NotImplementedError
@property
def vocab_size(self) -> Optional[int]:
raise NotImplementedError
@property
def embedding(self) -> Optional[Any]:
raise NotImplementedError
@property
def decoder(self) -> Optional[Any]:
raise NotImplementedError
@property
def max_sequence_length(self) -> Optional[int]:
raise NotImplementedError
@@ -0,0 +1,41 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from abc import ABC
from typing import Dict, Optional
from nemo.core.classes import NeuralModule
from nemo.core.neural_types import ChannelType, MaskType, NeuralType
from nemo.utils import logging
__all__ = ['EncoderModule']
class EncoderModule(NeuralModule, ABC):
""" Base class for encoder neural module to be used in NLP models. """
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"input_ids": NeuralType(('B', 'T'), ChannelType()),
"encoder_mask": NeuralType(('B', 'T'), MaskType()),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"last_hidden_states": NeuralType(('B', 'T', 'D'), ChannelType())}
@property
def hidden_size(self) -> Optional[int]:
raise NotImplementedError
@@ -0,0 +1,22 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.modules.common.huggingface.albert import AlbertEncoder
from nemo.collections.nlp.modules.common.huggingface.bert import BertEncoder
from nemo.collections.nlp.modules.common.huggingface.distilbert import DistilBertEncoder
from nemo.collections.nlp.modules.common.huggingface.huggingface_utils import (
get_huggingface_lm_model,
get_huggingface_pretrained_lm_models_list,
)
from nemo.collections.nlp.modules.common.huggingface.roberta import RobertaEncoder
@@ -0,0 +1,33 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from transformers import AlbertModel
from nemo.collections.nlp.modules.common.bert_module import BertModule
from nemo.core.classes import typecheck
__all__ = ['AlbertEncoder']
class AlbertEncoder(AlbertModel, BertModule):
"""
Wraps around the Huggingface transformers implementation repository for easy use within NeMo.
"""
@typecheck()
def forward(self, input_ids, attention_mask, token_type_ids):
res = super().forward(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)[0]
return res
@@ -0,0 +1,33 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from transformers import BertModel
from nemo.collections.nlp.modules.common.bert_module import BertModule
from nemo.core.classes import typecheck
__all__ = ['BertEncoder']
class BertEncoder(BertModel, BertModule):
"""
Wraps around the Huggingface transformers implementation repository for easy use within NeMo.
"""
@typecheck()
def forward(self, input_ids, attention_mask, token_type_ids):
res = super().forward(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)[0]
return res
@@ -0,0 +1,34 @@
# Copyright 2020 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from transformers import DistilBertModel
from nemo.collections.nlp.modules.common.bert_module import BertModule
from nemo.core.classes import typecheck
__all__ = ['DistilBertEncoder']
class DistilBertEncoder(DistilBertModel, BertModule):
"""
Wraps around the Huggingface transformers implementation repository for easy use within NeMo.
"""
@typecheck()
def forward(self, input_ids, attention_mask, token_type_ids=None):
# distilBert does not use token_type_ids as the most of the other Bert models
res = super().forward(input_ids=input_ids, attention_mask=attention_mask)[0]
return res
@@ -0,0 +1,79 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
from hydra.utils import instantiate
from transformers import AutoConfig, AutoModel
from nemo.collections.nlp.modules.common.decoder_module import DecoderModule
from nemo.collections.nlp.modules.common.huggingface.huggingface_utils import get_huggingface_pretrained_lm_models_list
from nemo.utils import logging
class HuggingFaceDecoderModule(DecoderModule):
"""Gets HuggingFace based model to be used as an Decoder in NeMo NLP.
Use the model_name arg to get a named model architecture.
Available model names can be found with get_huggingface_pretrained_lm_models_list() or
by going to https://huggingface.co/models.
Use the pretrained arg to get the named model architecture with or without pretrained weights.
If model_name is None, then we can pass in a custom configuration via the config_dict.
For example, to instantiate a HuggingFace BERT model with custom configuration we would do:
config_dict={
'_target_': 'transformers.BertConfig',
'hidden_size': 1536
}
Args:
model_name (Optional[str]): Named model architecture from HuggingFace. Defaults to None.
pretrained (bool): Use True to get pretrained weights.
False will use the same architecture but with randomly initialized weights.
Defaults to False.
config_dict (Optional[dict], optional): Use for custom configuration of the HuggingFace model. Defaults to None.
checkpoint_file (Optional[str], optional): Provide weights for the transformer from a local checkpoint. Defaults to None.
"""
def __init__(
self,
model_name: Optional[str] = None,
pretrained: bool = False,
config_dict: Optional[dict] = None,
checkpoint_file: Optional[str] = None,
):
super().__init__()
model = None
if model_name is not None:
if model_name in get_huggingface_pretrained_lm_models_list():
if pretrained:
model = AutoModel.from_pretrained(model_name)
else:
cfg = AutoConfig.from_pretrained(model_name)
model = AutoModel.from_config(cfg)
else:
logging.error(f'{model_name} not found in list of HuggingFace pretrained models')
else:
cfg = instantiate(config_dict)
model = AutoModel.from_config(cfg)
self._hidden_size = model.config.hidden_size
self._vocab_size = model.config.vocab_size
@property
def hidden_size(self) -> Optional[int]:
return self._hidden_size
@property
def vocab_size(self) -> Optional[int]:
return self._vocab_size
@@ -0,0 +1,99 @@
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
from hydra.utils import instantiate
from transformers import AutoConfig, AutoModel
from nemo.collections.nlp.modules.common.encoder_module import EncoderModule
from nemo.collections.nlp.modules.common.huggingface.huggingface_utils import get_huggingface_pretrained_lm_models_list
from nemo.core.classes.common import typecheck
from nemo.utils import logging
class HuggingFaceEncoderModule(EncoderModule):
""" Class for using HuggingFace encoders in NeMo NLP."""
def __init__(
self,
model_name: Optional[str] = None,
pretrained: bool = False,
config_dict: Optional[dict] = None,
checkpoint_file: Optional[str] = None,
):
"""Gets HuggingFace based model to be used as an Encoder in NeMo NLP.
Use the model_name arg to get a named model architecture.
Available model names can be found with get_huggingface_pretrained_lm_models_list() or
by going to https://huggingface.co/models.
Use the pretrained arg to get the named model architecture with or without pretrained weights.
If model_name is None, then we can pass in a custom configuration via the config_dict.
For example, to instantiate a HuggingFace BERT model with custom configuration we would do:
config_dict={
'_target_': 'transformers.BertConfig',
'hidden_size': 1536
}
Args:
model_name (Optional[str]): Named model architecture from HuggingFace. Defaults to None.
pretrained (bool): Use True to get pretrained weights.
False will use the same architecture but with randomly initialized weights.
Defaults to False.
config_dict (Optional[dict], optional): Use for custom configuration of the HuggingFace model. Defaults to None.
checkpoint_file (Optional[str], optional): Provide weights for the transformer from a local checkpoint. Defaults to None.
"""
super().__init__()
if checkpoint_file:
raise NotImplementedError('Restoring from checkpoint file not implemented yet.')
model = None
if model_name is not None:
if model_name in get_huggingface_pretrained_lm_models_list(include_external=True):
if pretrained:
config_dict.pop('vocab_size')
if config_dict:
raise ValueError(
f'When using pretrained model, config_dict should be None or empty. Got: {config_dict}'
)
model = AutoModel.from_pretrained(model_name)
else:
cfg = AutoConfig.from_pretrained(model_name)
model = AutoModel.from_config(cfg)
else:
logging.error(f'{model_name} not found in list of HuggingFace pretrained models')
else:
if pretrained:
raise ValueError(f'If not using model_name, then pretrained should be False. Got: {pretrained}.')
cfg = instantiate(config_dict)
model = AutoModel.from_config(cfg)
self._hidden_size = model.config.hidden_size
self._vocab_size = model.config.vocab_size
self._encoder = model
@typecheck()
def forward(self, input_ids, encoder_mask):
encoder_hidden_states = self._encoder.forward(input_ids=input_ids, attention_mask=encoder_mask)[0]
return encoder_hidden_states
@property
def hidden_size(self) -> Optional[int]:
return self._hidden_size
@property
def vocab_size(self) -> Optional[int]:
return self._vocab_size
@@ -0,0 +1,126 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from typing import List, Optional
from transformers import (
ALBERT_PRETRAINED_MODEL_ARCHIVE_LIST,
ALL_PRETRAINED_CONFIG_ARCHIVE_MAP,
BERT_PRETRAINED_MODEL_ARCHIVE_LIST,
DISTILBERT_PRETRAINED_MODEL_ARCHIVE_LIST,
ROBERTA_PRETRAINED_MODEL_ARCHIVE_LIST,
AlbertConfig,
AutoModel,
BertConfig,
DistilBertConfig,
RobertaConfig,
)
from nemo.collections.nlp.modules.common.huggingface.albert import AlbertEncoder
from nemo.collections.nlp.modules.common.huggingface.bert import BertEncoder
from nemo.collections.nlp.modules.common.huggingface.distilbert import DistilBertEncoder
from nemo.collections.nlp.modules.common.huggingface.roberta import RobertaEncoder
from nemo.utils import logging
__all__ = ["get_huggingface_lm_model", "get_huggingface_pretrained_lm_models_list"]
HUGGINGFACE_MODELS = {
"BertModel": {
"default": "bert-base-uncased",
"class": BertEncoder,
"config": BertConfig,
"pretrained_model_list": BERT_PRETRAINED_MODEL_ARCHIVE_LIST,
},
"DistilBertModel": {
"default": "distilbert-base-uncased",
"class": DistilBertEncoder,
"config": DistilBertConfig,
"pretrained_model_list": DISTILBERT_PRETRAINED_MODEL_ARCHIVE_LIST,
},
"RobertaModel": {
"default": "roberta-base",
"class": RobertaEncoder,
"config": RobertaConfig,
"pretrained_model_list": ROBERTA_PRETRAINED_MODEL_ARCHIVE_LIST,
},
"AlbertModel": {
"default": "albert-base-v2",
"class": AlbertEncoder,
"config": AlbertConfig,
"pretrained_model_list": ALBERT_PRETRAINED_MODEL_ARCHIVE_LIST,
},
}
def get_huggingface_lm_model(
pretrained_model_name: str, config_dict: Optional[dict] = None, config_file: Optional[str] = None,
):
"""
Returns lm model instantiated with Huggingface
Args:
pretrained_mode_name: specify this to instantiate pretrained model from Huggingface,
e.g. bert-base-cased. For entire list, see get_huggingface_pretrained_lm_models_list().
config_dict: model configuration dictionary used to instantiate Huggingface model from scratch
config_file: path to model configuration file used to instantiate Huggingface model from scratch
Returns:
BertModule
"""
try:
automodel = AutoModel.from_pretrained(pretrained_model_name)
except Exception as e:
raise ValueError(f"{pretrained_model_name} is not supported by HuggingFace. {e}")
model_type = type(automodel).__name__
if model_type in HUGGINGFACE_MODELS:
model_class = HUGGINGFACE_MODELS[model_type]["class"]
if config_file:
if not os.path.exists(config_file):
logging.warning(
f"Config file was not found at {config_file}. Will attempt to use config_dict or pretrained_model_name."
)
else:
config_class = HUGGINGFACE_MODELS[model_type]["config"]
return model_class(config_class.from_json_file(config_file))
if config_dict:
config_class = HUGGINGFACE_MODELS[model_type]["config"]
return model_class(config=config_class(**config_dict))
else:
return model_class.from_pretrained(pretrained_model_name)
else:
raise ValueError(f"Use HuffingFace API directly in NeMo for {pretrained_model_name}")
def get_huggingface_pretrained_lm_models_list(include_external: bool = False,) -> List[str]:
"""
Returns the list of pretrained HuggingFace language models
Args:
include_external if true includes all HuggingFace model names, not only those supported language models in NeMo.
Returns the list of HuggingFace models
"""
huggingface_models = []
if include_external:
huggingface_models = list(ALL_PRETRAINED_CONFIG_ARCHIVE_MAP.keys())
else:
for model in HUGGINGFACE_MODELS:
model_names = HUGGINGFACE_MODELS[model]["pretrained_model_list"]
huggingface_models.extend(model_names)
return huggingface_models
@@ -0,0 +1,34 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from transformers import RobertaModel
from nemo.collections.nlp.modules.common.bert_module import BertModule
from nemo.core.classes import typecheck
__all__ = ['RobertaEncoder']
class RobertaEncoder(RobertaModel, BertModule):
"""
Wraps around the Huggingface transformers implementation repository for easy use within NeMo.
"""
@typecheck()
def forward(self, input_ids, token_type_ids, attention_mask):
res = super().forward(input_ids=input_ids, attention_mask=attention_mask)[0]
return res
@@ -0,0 +1,179 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from dataclasses import dataclass
from typing import List, Optional, Union
from attr import asdict
from nemo.collections.nlp.modules.common.bert_module import BertModule
from nemo.collections.nlp.modules.common.decoder_module import DecoderModule
from nemo.collections.nlp.modules.common.encoder_module import EncoderModule
from nemo.collections.nlp.modules.common.huggingface.huggingface_utils import (
get_huggingface_lm_model,
get_huggingface_pretrained_lm_models_list,
)
from nemo.collections.nlp.modules.common.megatron.megatron_utils import (
get_megatron_lm_model,
get_megatron_lm_models_list,
)
from nemo.collections.nlp.modules.common.transformer.transformer import NeMoTransformerConfig
from nemo.collections.nlp.modules.common.transformer.transformer_utils import (
get_huggingface_transformer,
get_nemo_transformer,
)
from nemo.utils import logging
__all__ = ['get_pretrained_lm_models_list', 'get_lm_model']
def get_pretrained_lm_models_list(include_external: bool = False) -> List[str]:
"""
Returns the list of supported pretrained model names
Args:
include_external if true includes all HuggingFace model names, not only those supported language models in NeMo.
"""
return get_megatron_lm_models_list() + get_huggingface_pretrained_lm_models_list(include_external=include_external)
def get_lm_model(
pretrained_model_name: str,
config_dict: Optional[dict] = None,
config_file: Optional[str] = None,
checkpoint_file: Optional[str] = None,
vocab_file: Optional[str] = None,
) -> BertModule:
"""
Helper function to instantiate a language model encoder, either from scratch or a pretrained model.
If only pretrained_model_name are passed, a pretrained model is returned.
If a configuration is passed, whether as a file or dictionary, the model is initialized with random weights.
Args:
pretrained_model_name: pretrained model name, for example, bert-base-uncased or megatron-bert-cased.
See get_pretrained_lm_models_list() for full list.
config_dict: path to the model configuration dictionary
config_file: path to the model configuration file
checkpoint_file: path to the pretrained model checkpoint
vocab_file: path to vocab_file to be used with Megatron-LM
Returns:
Pretrained BertModule
"""
# check valid model type
if not pretrained_model_name or pretrained_model_name not in get_pretrained_lm_models_list(include_external=False):
logging.warning(
f'{pretrained_model_name} is not in get_pretrained_lm_models_list(include_external=False), '
f'will be using AutoModel from HuggingFace.'
)
# warning when user passes both configuration dict and file
if config_dict and config_file:
logging.warning(
f"Both config_dict and config_file were found, defaulting to use config_file: {config_file} will be used."
)
if "megatron" in pretrained_model_name:
model, checkpoint_file = get_megatron_lm_model(
config_dict=config_dict,
config_file=config_file,
pretrained_model_name=pretrained_model_name,
checkpoint_file=checkpoint_file,
vocab_file=vocab_file,
)
else:
model = get_huggingface_lm_model(
config_dict=config_dict, config_file=config_file, pretrained_model_name=pretrained_model_name,
)
if checkpoint_file and os.path.exists(checkpoint_file):
model.restore_weights(restore_path=checkpoint_file)
return model
# @dataclass
# class TransformerConfig:
# library: str = 'nemo'
# model_name: Optional[str] = None
# pretrained: bool = False
# config_dict: Optional[dict] = None
# checkpoint_file: Optional[str] = None
# encoder: bool = True
def get_transformer(
library: str = 'nemo',
model_name: Optional[str] = None,
pretrained: bool = False,
config_dict: Optional[dict] = None,
checkpoint_file: Optional[str] = None,
encoder: bool = True,
pre_ln_final_layer_norm=True,
) -> Union[EncoderModule, DecoderModule]:
"""Gets Transformer based model to be used as an Encoder or Decoder in NeMo NLP.
First choose the library to get the transformer from. This can be huggingface,
megatron, or nemo. Use the model_name arg to get a named model architecture
and use the pretrained arg to get the named model architecture with pretrained weights.
If model_name is None, then we can pass in a custom configuration via the config_dict.
For example, to instantiate a HuggingFace BERT model with custom configuration we would do:
encoder = get_transformer(library='huggingface',
config_dict={
'_target_': 'transformers.BertConfig',
'hidden_size': 1536
})
Args:
library (str, optional): Can be 'nemo', 'huggingface', or 'megatron'. Defaults to 'nemo'.
model_name (Optional[str], optional): Named model architecture from the chosen library. Defaults to None.
pretrained (bool, optional): Use True to get pretrained weights.
False will use the same architecture but with randomly initialized weights.
Defaults to False.
config_dict (Optional[dict], optional): Use for custom configuration of transformer. Defaults to None.
checkpoint_file (Optional[str], optional): Provide weights for the transformer from a local checkpoint. Defaults to None.
encoder (bool, optional): True returns an EncoderModule, False returns a DecoderModule. Defaults to True.
Returns:
Union[EncoderModule, DecoderModule]: Ensures that Encoder/Decoder will work in EncDecNLPModel
"""
model = None
if library == 'nemo':
if isinstance(config_dict, NeMoTransformerConfig):
config_dict = asdict(config_dict)
model = get_nemo_transformer(
model_name=model_name,
pretrained=pretrained,
config_dict=config_dict,
encoder=encoder,
pre_ln_final_layer_norm=pre_ln_final_layer_norm,
)
if checkpoint_file is not None:
if os.path.isfile(checkpoint_file):
raise ValueError(f'Loading transformer weights from checkpoint file has not been implemented yet.')
elif library == 'huggingface':
model = get_huggingface_transformer(
model_name=model_name, pretrained=pretrained, config_dict=config_dict, encoder=encoder
)
return model
@@ -0,0 +1,19 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.collections.nlp.modules.common.megatron.megatron_bert import MegatronBertEncoder
from nemo.collections.nlp.modules.common.megatron.megatron_utils import (
get_megatron_checkpoint,
get_megatron_lm_models_list,
)
@@ -0,0 +1,216 @@
# Copyright 2018 The Google AI Language Team Authors and
# The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import torch
from megatron import get_args, initialize_megatron
from megatron.checkpointing import set_checkpoint_version
from megatron.model import get_language_model
from megatron.model.bert_model import bert_attention_mask_func, bert_extended_attention_mask, bert_position_ids
from megatron.mpu import (
get_model_parallel_group,
model_parallel_is_initialized,
set_pipeline_model_parallel_rank,
set_pipeline_model_parallel_world_size,
)
from omegaconf import DictConfig, OmegaConf
from nemo.collections.nlp.modules.common.bert_module import BertModule
from nemo.core.classes import typecheck
from nemo.utils import logging
from nemo.utils.app_state import AppState
__all__ = ['MegatronBertEncoder']
def complete_lazy_init(self):
# finish megatron-lm initialization
if hasattr(self, "_lazy_init_fn") and self._lazy_init_fn is not None:
self._lazy_init_fn()
self._lazy_init_fn = None
class MegatronBertEncoder(BertModule):
"""
MegatronBERT wraps around the Megatron Language model
from https://github.com/NVIDIA/Megatron-LM
Args:
config_file (str): path to model configuration file.
vocab_file (str): path to vocabulary file.
tokenizer_type (str): tokenizer type, currently only 'BertWordPieceLowerCase' supported.
"""
def __init__(self, model_name, config, vocab_file, model_parallel_size=None, model_parallel_rank=None):
super().__init__()
self._model_parallel_size = model_parallel_size
self._model_parallel_rank = model_parallel_rank
self._restore_path = None
self._app_state = None
self._model_name = model_name
if not os.path.exists(vocab_file):
raise ValueError(f'Vocab file not found at {vocab_file}')
# convert config to dictionary
if isinstance(config, DictConfig):
config = OmegaConf.to_container(config)
config["vocab_file"] = vocab_file
config['tokenizer_type'] = 'BertWordPieceLowerCase'
config['lazy_mpu_init'] = True
config['onnx_safe'] = True
# if 'model_parallel_size' in config:
if self._model_parallel_size is not None:
app_state = AppState()
self._app_state = app_state
# must be set for model parallel megatron-lm
os.environ["WORLD_SIZE"] = str(app_state.world_size)
os.environ["RANK"] = str(self._model_parallel_rank)
extra_args_provider = self._update_megatron_args(tensor_model_parallel_size=self._model_parallel_size)
else:
extra_args_provider = self._update_megatron_args()
# configure globals for megatron
set_pipeline_model_parallel_rank(0) # pipeline model parallelism not implemented in NeMo
set_pipeline_model_parallel_world_size(1) # pipeline model parallelism not implemented in NeMo
# Initialize part of Megatron global state that is needed for its constructor.
# We set 'lazy_mpu_init' flag on to make Megatron do only the initialization that does not depend
# on ddp be initialized yet (and we don't want Megatron to initialize DDP itself either)
# and to return a hook for us to call after PTL has torch.distributed initialized.
# (or if no PTL in case of inference - then we'll initialize torch.distributed)
# We call and clear this hook on first call to forward()
self._lazy_init_fn = initialize_megatron(
extra_args_provider=extra_args_provider, args_defaults=config, ignore_unknown_args=True
)
# read Megatron arguments back
args = get_args()
logging.info(f'Megatron-lm argparse args: {args}')
self.language_model, self._language_model_key = get_language_model(
attention_mask_func=bert_attention_mask_func, num_tokentypes=2, add_pooler=False
)
self.config = OmegaConf.create(config)
# key used for checkpoints
self._hidden_size = self.language_model.hidden_size
def _update_megatron_args(
self,
micro_batch_size=1,
tensor_model_parallel_size=1,
scaled_masked_softmax_fusion=False,
bias_gelu_fusion=False,
bias_dropout_fusion=False,
):
def extra_args_provider(parser):
parser.set_defaults(micro_batch_size=micro_batch_size)
parser.set_defaults(tensor_model_parallel_size=tensor_model_parallel_size)
parser.set_defaults(scaled_masked_softmax_fusion=scaled_masked_softmax_fusion)
parser.set_defaults(bias_gelu_fusion=bias_gelu_fusion)
parser.set_defaults(bias_dropout_fusion=bias_dropout_fusion)
return parser
return extra_args_provider
def complete_lazy_init(self):
# finish megatron-lm initialization
if hasattr(self, "_lazy_init_fn") and self._lazy_init_fn is not None:
self._lazy_init_fn()
self._lazy_init_fn = None
@property
def hidden_size(self):
"""
Property returning hidden size.
Returns:
Hidden size.
"""
return self._hidden_size
@typecheck()
def forward(self, input_ids, attention_mask, token_type_ids):
app_state = AppState()
if app_state.model_parallel_size is None:
self.complete_lazy_init()
extended_attention_mask = bert_extended_attention_mask(attention_mask)
position_ids = bert_position_ids(input_ids)
sequence_output = self.language_model(
input_ids=input_ids,
position_ids=position_ids,
attention_mask=extended_attention_mask,
tokentype_ids=token_type_ids,
)
return sequence_output
def _load_checkpoint(self, filename):
"""Helper function for loading megatron checkpoints.
Args:
filename (str): Path to megatron checkpoint.
"""
state_dict = torch.load(filename, map_location='cpu')
if 'checkpoint_version' in state_dict:
if state_dict['checkpoint_version'] is not None:
set_checkpoint_version(state_dict['checkpoint_version'])
logging.info(
f"Megatron-lm checkpoint version found. Setting checkpoint_version to {state_dict['checkpoint_version']}."
)
else:
logging.warning('Megatron-lm checkpoint version not found. Setting checkpoint_version to 0.')
set_checkpoint_version(0)
# to load from Megatron pretrained checkpoint
if 'model' in state_dict:
self.language_model.load_state_dict(state_dict['model'][self._language_model_key])
else:
self.load_state_dict(state_dict)
logging.info(f"Checkpoint loaded from from {filename}")
def restore_weights(self, restore_path: str):
"""Restores module/model's weights.
For model parallel checkpoints the directory structure
should be restore_path/mp_rank_0X/model_optim_rng.pt
Args:
restore_path (str): restore_path should a file or a directory if using model parallel
"""
self._restore_path = restore_path
if os.path.isfile(restore_path):
self._load_checkpoint(restore_path)
elif os.path.isdir(restore_path):
# need model parallel groups to restore model parallel checkpoints
if model_parallel_is_initialized():
model_parallel_rank = torch.distributed.get_rank(group=get_model_parallel_group())
mp_restore_path = f'{restore_path}/mp_rank_{model_parallel_rank:02d}/model_optim_rng.pt'
self._load_checkpoint(mp_restore_path)
else:
logging.info(f'torch.distributed not initialized yet. Will not restore model parallel checkpoint')
else:
logging.error(f'restore_path: {restore_path} must be a file or directory.')
@@ -0,0 +1,307 @@
# Copyright 2020 The HuggingFace Inc. team.
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import glob
import json
import os
from typing import Dict, List, Optional, Tuple
import torch
import wget
from torch.hub import _get_torch_home
from nemo.collections.nlp.modules.common.megatron.megatron_bert import MegatronBertEncoder
from nemo.utils import AppState, logging
__all__ = [
"get_megatron_lm_model",
"get_megatron_lm_models_list",
"get_megatron_checkpoint",
"is_lower_cased_megatron",
"get_megatron_tokenizer",
]
torch_home = _get_torch_home()
if not isinstance(torch_home, str):
logging.info("Torch home not found, caching megatron in cwd")
torch_home = os.getcwd()
MEGATRON_CACHE = os.path.join(torch_home, "megatron")
CONFIGS = {"345m": {"hidden_size": 1024, "num_attention_heads": 16, "num_layers": 24, "max_position_embeddings": 512}}
MEGATRON_CONFIG_MAP = {
"megatron-bert-345m-uncased": {
"config": CONFIGS["345m"],
"checkpoint": "https://api.ngc.nvidia.com/v2/models/nvidia/megatron_bert_345m/versions/v0.0/files/release/mp_rank_00/model_optim_rng.pt",
"vocab": "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased-vocab.txt",
"do_lower_case": True,
"tokenizer_name": "bert-large-uncased",
},
"megatron-bert-345m-cased": {
"config": CONFIGS["345m"],
"checkpoint": "https://api.ngc.nvidia.com/v2/models/nvidia/megatron_bert_345m/versions/v0.1_cased/files/release/mp_rank_00/model_optim_rng.pt",
"vocab": "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased-vocab.txt",
"do_lower_case": False,
"tokenizer_name": "bert-large-cased",
},
"megatron-bert-uncased": {
"config": None,
"checkpoint": None,
"vocab": "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased-vocab.txt",
"do_lower_case": True,
"tokenizer_name": "bert-large-uncased",
},
"megatron-bert-cased": {
"config": None,
"checkpoint": None,
"vocab": "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased-vocab.txt",
"do_lower_case": False,
"tokenizer_name": "bert-large-cased",
},
"biomegatron-bert-345m-uncased": {
"config": CONFIGS["345m"],
"checkpoint": "https://api.ngc.nvidia.com/v2/models/nvidia/biomegatron345muncased/versions/0/files/MegatronBERT.pt",
"vocab": "https://api.ngc.nvidia.com/v2/models/nvidia/biomegatron345muncased/versions/0/files/vocab.txt",
"do_lower_case": True,
"tokenizer_name": "bert-large-uncased",
},
"biomegatron-bert-345m-cased": {
"config": CONFIGS["345m"],
"checkpoint": "https://api.ngc.nvidia.com/v2/models/nvidia/biomegatron345mcased/versions/0/files/MegatronBERT.pt",
"vocab": "https://api.ngc.nvidia.com/v2/models/nvidia/biomegatron345mcased/versions/0/files/vocab.txt",
"do_lower_case": False,
"tokenizer_name": "bert-large-cased",
},
}
def get_megatron_lm_model(
pretrained_model_name: str,
config_dict: Optional[dict] = None,
config_file: Optional[str] = None,
checkpoint_file: Optional[str] = None,
vocab_file: Optional[str] = None,
) -> Tuple[MegatronBertEncoder, str]:
"""
Returns MegatronBertEncoder and a default or user specified path to the checkpoint file
Args:
pretrained_mode_name: model name from MEGATRON_CONFIG_MAP
for example: megatron-bert-cased
config_dict: model configuration parameters
config_file: path to model configuration file. Takes precedence over config_dict if both supplied.
checkpoint_file: path to checkpoint file or directory if using model parallel.
vocab_file: path to vocab file
Returns:
model: MegatronBertEncoder
checkpoint_file: path to checkpoint file or directory
"""
config = None
# get default config and checkpoint
if config_file:
with open(config_file) as f:
config = json.load(f)
# replace dashes with underscores in config keys
fixed_config = {}
for key in config.keys():
fixed_key = key.replace("-", "_")
if fixed_key == 'max_seq_length':
fixed_key = 'max_position_embeddings'
fixed_config[fixed_key] = config[key]
# 'vocab_size" no longer used.
if 'vocab_size' in fixed_config:
fixed_config.pop('vocab_size')
config = fixed_config
elif config_dict:
config = config_dict
elif pretrained_model_name in get_megatron_lm_models_list():
config = get_megatron_config(pretrained_model_name)
else:
raise ValueError(f"{pretrained_model_name} is not supported")
if config is None:
raise ValueError(f"config_file or config_dict is required for {pretrained_model_name}")
if not checkpoint_file:
checkpoint_file = get_megatron_checkpoint(pretrained_model_name)
if not vocab_file:
vocab_file = get_megatron_vocab_file(pretrained_model_name)
app_state = AppState()
if app_state.model_parallel_size is not None and app_state.model_parallel_rank is not None:
# model parallel already known from .nemo restore
model_parallel_size = app_state.model_parallel_size
model_parallel_rank = app_state.model_parallel_rank
elif os.path.isdir(checkpoint_file):
# starting training from megatron-lm checkpoint
mp_ranks = glob.glob(os.path.join(checkpoint_file, 'mp_rank*'))
model_parallel_size = len(mp_ranks)
app_state.model_parallel_size = model_parallel_size
logging.info(
(
f'restore_path: {checkpoint_file} is a directory. '
f'Assuming megatron model parallelism with '
f'model_parallel_size: {model_parallel_size}'
)
)
# try to get local rank from global
local_rank = None
try:
local_rank = int(os.environ['LOCAL_RANK'])
except:
logging.info('Global variable LOCAL_RANK not yet specified')
if local_rank is not None:
app_state.local_rank = local_rank
else:
# if local is None then we are on the main process
local_rank = 0
model_parallel_rank = compute_model_parallel_rank(local_rank, model_parallel_size)
app_state.model_parallel_rank = model_parallel_rank
else:
model_parallel_size = None
model_parallel_rank = None
model = MegatronBertEncoder(
model_name=pretrained_model_name,
config=config,
vocab_file=vocab_file,
model_parallel_size=model_parallel_size,
model_parallel_rank=model_parallel_rank,
)
return model, checkpoint_file
def compute_model_parallel_rank(local_rank, model_parallel_size):
return local_rank % model_parallel_size
def get_megatron_lm_models_list() -> List[str]:
"""
Returns the list of supported Megatron-LM models
"""
return list(MEGATRON_CONFIG_MAP.keys())
def get_megatron_config(pretrained_model_name: str) -> Dict[str, int]:
"""
Returns Megatron-LM model config file
Args:
pretrained_model_name (str): pretrained model name
Returns:
config (dict): contains model configuration: number of hidden layers, number of attention heads, etc
"""
_check_megatron_name(pretrained_model_name)
return MEGATRON_CONFIG_MAP[pretrained_model_name]["config"]
def _check_megatron_name(pretrained_model_name: str) -> None:
megatron_model_list = get_megatron_lm_models_list()
if pretrained_model_name not in megatron_model_list:
raise ValueError(f'For Megatron-LM models, choose from the following list: {megatron_model_list}')
def get_megatron_vocab_file(pretrained_model_name: str) -> str:
"""
Gets vocabulary file from cache or downloads it
Args:
pretrained_model_name: pretrained model name
Returns:
path: path to the vocab file
"""
_check_megatron_name(pretrained_model_name)
url = MEGATRON_CONFIG_MAP[pretrained_model_name]["vocab"]
path = os.path.join(MEGATRON_CACHE, pretrained_model_name + "_vocab")
path = _download(path, url)
return path
def get_megatron_checkpoint(pretrained_model_name: str) -> str:
"""
Gets checkpoint file from cache or downloads it
Args:
pretrained_model_name: pretrained model name
Returns:
path: path to model checkpoint
"""
_check_megatron_name(pretrained_model_name)
url = MEGATRON_CONFIG_MAP[pretrained_model_name]["checkpoint"]
path = os.path.join(MEGATRON_CACHE, pretrained_model_name)
return _download(path, url)
def _download(path: str, url: str):
"""
Gets a file from cache or downloads it
Args:
path: path to the file in cache
url: url to the file
Returns:
path: path to the file in cache
"""
if url is None:
return None
if not os.path.exists(path):
master_device = not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0
if not os.path.exists(path):
if master_device:
os.makedirs(MEGATRON_CACHE, exist_ok=True)
logging.info(f"Downloading from {url}")
wget.download(url, path)
# wait until the master process downloads the file and writes it to the cache dir
if torch.distributed.is_initialized():
torch.distributed.barrier()
return path
def is_lower_cased_megatron(pretrained_model_name):
"""
Returns if the megatron is cased or uncased
Args:
pretrained_model_name (str): pretrained model name
Returns:
do_lower_cased (bool): whether the model uses lower cased data
"""
_check_megatron_name(pretrained_model_name)
return MEGATRON_CONFIG_MAP[pretrained_model_name]["do_lower_case"]
def get_megatron_tokenizer(pretrained_model_name: str):
"""
Takes a pretrained_model_name for megatron such as "megatron-bert-cased" and returns the according
tokenizer name for tokenizer instantiating.
Args:
pretrained_model_name: pretrained_model_name for megatron such as "megatron-bert-cased"
Returns:
tokenizer name for tokenizer instantiating
"""
_check_megatron_name(pretrained_model_name)
return MEGATRON_CONFIG_MAP[pretrained_model_name]["tokenizer_name"]
@@ -0,0 +1,72 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Optional
from nemo.collections.common.parts import MultiLayerPerceptron
from nemo.collections.nlp.modules.common.classifier import Classifier
from nemo.core.classes import typecheck
from nemo.core.neural_types import LogitsType, LogprobsType, NeuralType
__all__ = ['SequenceClassifier']
class SequenceClassifier(Classifier):
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
if not self.log_softmax:
return {"logits": NeuralType(('B', 'D'), LogitsType())}
else:
return {"log_probs": NeuralType(('B', 'D'), LogprobsType())}
def __init__(
self,
hidden_size: int,
num_classes: int,
num_layers: int = 2,
activation: str = 'relu',
log_softmax: bool = True,
dropout: float = 0.0,
use_transformer_init: bool = True,
idx_conditioned_on: int = 0,
):
"""
Initializes the SequenceClassifier module.
Args:
hidden_size: the hidden size of the mlp head on the top of the encoder
num_classes: number of the classes to predict
num_layers: number of the linear layers of the mlp head on the top of the encoder
activation: type of activations between layers of the mlp head
log_softmax: applies the log softmax on the output
dropout: the dropout used for the mlp head
use_transformer_init: initializes the weights with the same approach used in Transformer
idx_conditioned_on: index of the token to use as the sequence representation for the classification task, default is the first token
"""
super().__init__(hidden_size=hidden_size, dropout=dropout)
self.log_softmax = log_softmax
self._idx_conditioned_on = idx_conditioned_on
self.mlp = MultiLayerPerceptron(
hidden_size=hidden_size,
num_classes=num_classes,
num_layers=num_layers,
activation=activation,
log_softmax=log_softmax,
)
self.post_init(use_transformer_init=use_transformer_init)
@typecheck()
def forward(self, hidden_states):
hidden_states = self.dropout(hidden_states)
logits = self.mlp(hidden_states[:, self._idx_conditioned_on])
return logits
@@ -0,0 +1,69 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Optional
from torch import Tensor
from nemo.collections.common.parts import MultiLayerPerceptron
from nemo.collections.nlp.modules.common.classifier import Classifier
from nemo.core.classes import typecheck
from nemo.core.neural_types import NeuralType, RegressionValuesType
__all__ = ['SequenceRegression']
class SequenceRegression(Classifier):
"""
Args:
hidden_size: the hidden size of the mlp head on the top of the encoder
num_layers: number of the linear layers of the mlp head on the top of the encoder
activation: type of activations between layers of the mlp head
dropout: the dropout used for the mlp head
use_transformer_init: initializes the weights with the same approach used in Transformer
idx_conditioned_on: index of the token to use as the sequence representation for the classification task,
default is the first token
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"preds": NeuralType(tuple('B'), RegressionValuesType())}
def __init__(
self,
hidden_size: int,
num_layers: int = 2,
activation: str = 'relu',
dropout: float = 0.0,
use_transformer_init: bool = True,
idx_conditioned_on: int = 0,
):
""" Initializes the SequenceRegression module. """
super().__init__(hidden_size=hidden_size, dropout=dropout)
self._idx_conditioned_on = idx_conditioned_on
self.mlp = MultiLayerPerceptron(
hidden_size, num_classes=1, num_layers=num_layers, activation=activation, log_softmax=False,
)
self.post_init(use_transformer_init=use_transformer_init)
@typecheck()
def forward(self, hidden_states: Tensor) -> Tensor:
""" Forward pass through the module.
Args:
hidden_states: hidden states for each token in a sequence, for example, BERT module output
"""
hidden_states = self.dropout(hidden_states)
preds = self.mlp(hidden_states[:, self._idx_conditioned_on])
return preds.view(-1)
@@ -0,0 +1,80 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Optional
from nemo.collections.common.parts import MultiLayerPerceptron
from nemo.collections.nlp.modules.common.classifier import Classifier
from nemo.core.classes import typecheck
from nemo.core.neural_types import LogitsType, NeuralType
__all__ = ['SequenceTokenClassifier']
class SequenceTokenClassifier(Classifier):
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"intent_logits": NeuralType(('B', 'D'), LogitsType()),
"slot_logits": NeuralType(('B', 'T', 'D'), LogitsType()),
}
def __init__(
self,
hidden_size: int,
num_intents: int,
num_slots: int,
num_layers: int = 2,
activation: str = 'relu',
log_softmax: bool = False,
dropout: float = 0.0,
use_transformer_init: bool = True,
):
"""
Initializes the SequenceTokenClassifier module, could be used for tasks that train sequence and
token classifiers jointly, for example, for intent detection and slot tagging task.
Args:
hidden_size: hidden size of the mlp head on the top of the encoder
num_intents: number of the intents to predict
num_slots: number of the slots to predict
num_layers: number of the linear layers of the mlp head on the top of the encoder
activation: type of activations between layers of the mlp head
log_softmax: applies the log softmax on the output
dropout: the dropout used for the mlp head
use_transformer_init: initializes the weights with the same approach used in Transformer
"""
super().__init__(hidden_size=hidden_size, dropout=dropout)
self.intent_mlp = MultiLayerPerceptron(
hidden_size=hidden_size,
num_classes=num_intents,
num_layers=num_layers,
activation=activation,
log_softmax=log_softmax,
)
self.slot_mlp = MultiLayerPerceptron(
hidden_size=hidden_size,
num_classes=num_slots,
num_layers=num_layers,
activation=activation,
log_softmax=log_softmax,
)
self.post_init(use_transformer_init=use_transformer_init)
@typecheck()
def forward(self, hidden_states):
hidden_states = self.dropout(hidden_states)
# intent is classified by first hidden position
intent_logits = self.intent_mlp(hidden_states[:, 0])
slot_logits = self.slot_mlp(hidden_states)
return intent_logits, slot_logits
@@ -0,0 +1,165 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from typing import Dict, Optional
from omegaconf.omegaconf import MISSING
from torch import nn as nn
from nemo.collections.common.parts import MultiLayerPerceptron
from nemo.collections.nlp.modules.common.classifier import Classifier
from nemo.core.classes import typecheck
from nemo.core.neural_types import LogitsType, LogprobsType, NeuralType
__all__ = ['BertPretrainingTokenClassifier', 'TokenClassifier']
ACT2FN = {"gelu": nn.functional.gelu, "relu": nn.functional.relu}
@dataclass
class TokenClassifierConfig:
num_layers: int = 1
activation: str = 'relu'
log_softmax: bool = True
dropout: float = 0.0
use_transformer_init: bool = True
class TokenClassifier(Classifier):
"""
A module to perform token level classification tasks such as Named entity recognition.
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""
Returns definitions of module output ports.
"""
if not self.log_softmax:
return {"logits": NeuralType(('B', 'T', 'C'), LogitsType())}
else:
return {"log_probs": NeuralType(('B', 'T', 'C'), LogprobsType())}
def __init__(
self,
hidden_size: int,
num_classes: int,
num_layers: int = 1,
activation: str = 'relu',
log_softmax: bool = True,
dropout: float = 0.0,
use_transformer_init: bool = True,
) -> None:
"""
Initializes the Token Classifier module.
Args:
hidden_size: the size of the hidden dimension
num_classes: number of classes
num_layers: number of fully connected layers in the multilayer perceptron (MLP)
activation: activation to usee between fully connected layers in the MLP
log_softmax: whether to apply softmax to the output of the MLP
dropout: dropout to apply to the input hidden states
use_transformer_init: whether to initialize the weights of the classifier head with the same approach used in Transformer
"""
super().__init__(hidden_size=hidden_size, dropout=dropout)
self.log_softmax = log_softmax
self.mlp = MultiLayerPerceptron(
hidden_size, num_classes, num_layers=num_layers, activation=activation, log_softmax=log_softmax
)
self.post_init(use_transformer_init=use_transformer_init)
@typecheck()
def forward(self, hidden_states):
"""
Performs the forward step of the module.
Args:
hidden_states: batch of hidden states (for example, from the BERT encoder module)
[BATCH_SIZE x SEQ_LENGTH x HIDDEN_SIZE]
Returns: logits value for each class [BATCH_SIZE x SEQ_LENGTH x NUM_CLASSES]
"""
hidden_states = self.dropout(hidden_states)
logits = self.mlp(hidden_states)
return logits
class BertPretrainingTokenClassifier(Classifier):
"""
A module to perform token level classification tasks for Bert pretraining.
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""
Returns definitions of module output ports.
"""
if not self.log_softmax:
return {"logits": NeuralType(('B', 'T', 'C'), LogitsType())}
else:
return {"log_probs": NeuralType(('B', 'T', 'C'), LogprobsType())}
def __init__(
self,
hidden_size: int,
num_classes: int,
num_layers: int = 1,
activation: str = 'relu',
log_softmax: bool = True,
dropout: float = 0.0,
use_transformer_init: bool = True,
) -> None:
"""
Initializes the Token Classifier module.
Args:
hidden_size: the size of the hidden dimension
num_classes: number of classes
num_layers: number of fully connected layers in the multilayer perceptron (MLP)
activation: activation to usee between fully connected layers in the MLP
log_softmax: whether to apply softmax to the output of the MLP
dropout: dropout to apply to the input hidden states
use_transformer_init: whether to initialize the weights of the classifier head with the same approach used in Transformer
"""
super().__init__(hidden_size=hidden_size, dropout=dropout)
self.log_softmax = log_softmax
if activation not in ACT2FN:
raise ValueError(f'activation "{activation}" not found')
self.dense = nn.Linear(hidden_size, hidden_size)
self.act = ACT2FN[activation]
self.norm = nn.LayerNorm(hidden_size, eps=1e-12)
self.mlp = MultiLayerPerceptron(
hidden_size, num_classes, num_layers=num_layers, activation=activation, log_softmax=log_softmax
)
self.post_init(use_transformer_init=use_transformer_init)
@typecheck()
def forward(self, hidden_states):
"""
Performs the forward step of the module.
Args:
hidden_states: batch of hidden states (for example, from the BERT encoder module)
[BATCH_SIZE x SEQ_LENGTH x HIDDEN_SIZE]
Returns: logits value for each class [BATCH_SIZE x SEQ_LENGTH x NUM_CLASSES]
"""
hidden_states = self.dropout(hidden_states)
hidden_states = self.dense(hidden_states)
hidden_states = self.act(hidden_states)
transform = self.norm(hidden_states)
logits = self.mlp(transform)
return logits
@@ -0,0 +1,144 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os.path
from dataclasses import MISSING, dataclass
from os import path
from typing import Dict, List, Optional
import nemo
from nemo.collections.common.tokenizers.char_tokenizer import CharTokenizer
from nemo.collections.common.tokenizers.huggingface.auto_tokenizer import AutoTokenizer
from nemo.collections.common.tokenizers.word_tokenizer import WordTokenizer
from nemo.collections.common.tokenizers.youtokentome_tokenizer import YouTokenToMeTokenizer
from nemo.collections.nlp.modules.common.huggingface.huggingface_utils import get_huggingface_pretrained_lm_models_list
from nemo.collections.nlp.modules.common.lm_utils import get_pretrained_lm_models_list
from nemo.collections.nlp.modules.common.megatron.megatron_utils import get_megatron_tokenizer
from nemo.utils import logging
__all__ = ['get_tokenizer', 'get_tokenizer_list']
def get_tokenizer_list() -> List[str]:
"""
Returns all all supported tokenizer names
"""
s = set(get_pretrained_lm_models_list())
s.update(set(get_huggingface_pretrained_lm_models_list(include_external=True)))
return ["sentencepiece", "char", "word"] + list(s)
@dataclass
class TokenizerConfig:
library: str = MISSING
tokenizer_model: Optional[str] = None
vocab_size: Optional[int] = None
vocab_file: Optional[str] = None
special_tokens: Optional[Dict[str, str]] = None
bpe_dropout: Optional[float] = 0.0
coverage: Optional[float] = 0.999
training_sample_size: Optional[int] = None
def get_tokenizer(
tokenizer_name: str,
tokenizer_model: Optional[str] = None,
vocab_file: Optional[str] = None,
special_tokens: Optional[Dict[str, str]] = None,
use_fast: Optional[bool] = False,
bpe_dropout: Optional[float] = 0.0,
):
"""
Args:
tokenizer_name: sentencepiece or pretrained model from the hugging face list,
for example: bert-base-cased
To see the list of all HuggingFace pretrained models, use: nemo_nlp.modules.common.get_huggingface_pretrained_lm_models_list()
tokenizer_model: tokenizer model file of sentencepiece or youtokentome
special_tokens: dict of special tokens
vocab_file: path to vocab file
use_fast: (only for HuggingFace AutoTokenizer) set to True to use fast HuggingFace tokenizer
bpe_dropout: (only supported by YTTM tokenizer) BPE dropout tries to corrupt the standard segmentation procedure of BPE to help
model better learn word compositionality and become robust to segmentation errors.
It has emperically been shown to improve inference time BLEU scores.
"""
if special_tokens is None:
special_tokens_dict = {}
else:
special_tokens_dict = special_tokens
if 'megatron' in tokenizer_name:
if vocab_file is None:
vocab_file = nemo.collections.nlp.modules.common.megatron.megatron_utils.get_megatron_vocab_file(
tokenizer_name
)
tokenizer_name = get_megatron_tokenizer(tokenizer_name)
if tokenizer_name == 'sentencepiece':
return nemo.collections.common.tokenizers.sentencepiece_tokenizer.SentencePieceTokenizer(
model_path=tokenizer_model, special_tokens=special_tokens, legacy=True
)
elif tokenizer_name == 'yttm':
return YouTokenToMeTokenizer(model_path=tokenizer_model, bpe_dropout=bpe_dropout)
elif tokenizer_name == 'word':
return WordTokenizer(vocab_file=vocab_file, **special_tokens_dict)
elif tokenizer_name == 'char':
return CharTokenizer(vocab_file=vocab_file, **special_tokens_dict)
return AutoTokenizer(
pretrained_model_name=tokenizer_name, vocab_file=vocab_file, **special_tokens_dict, use_fast=use_fast
)
def get_nmt_tokenizer(
library: str = 'yttm',
model_name: Optional[str] = None,
tokenizer_model: Optional[str] = None,
vocab_file: Optional[str] = None,
special_tokens: Optional[Dict[str, str]] = None,
use_fast: Optional[bool] = False,
bpe_dropout: Optional[float] = 0.0,
):
"""
Args:
model_name: if using a pretrained model from NeMo or HuggingFace
tokenizer_model: tokenizer model file of sentencepiece or youtokentome
special_tokens: dict of special tokens
vocab_file: path to vocab file
use_fast: (only for HuggingFace AutoTokenizer) set to True to use fast HuggingFace tokenizer
bpe_dropout: (only supported by YTTM tokenizer) BPE dropout tries to corrupt the standard segmentation procedure of BPE to help
model better learn word compositionality and become robust to segmentation errors.
It has emperically been shown to improve inference time BLEU scores.
"""
if special_tokens is None:
special_tokens_dict = {}
else:
special_tokens_dict = special_tokens
if library == 'yttm':
logging.info(f'Getting YouTokenToMeTokenizer with model: {tokenizer_model}.')
return YouTokenToMeTokenizer(model_path=tokenizer_model, bpe_dropout=bpe_dropout)
elif library == 'huggingface':
logging.info(f'Getting HuggingFace AutoTokenizer with pretrained_model_name: {model_name}')
return AutoTokenizer(
pretrained_model_name=model_name, vocab_file=vocab_file, **special_tokens_dict, use_fast=use_fast
)
elif library == 'sentencepiece':
logging.info(f'Getting SentencePiece with model: {model_name}')
return nemo.collections.common.tokenizers.sentencepiece_tokenizer.SentencePieceTokenizer(
model_path=tokenizer_model, special_tokens=special_tokens_dict
)
else:
raise NotImplementedError(
'Currently we only support "yttm", "huggingface", and "sentencepiece" tokenizer library.'
)

Some files were not shown because too many files have changed in this diff Show More