Hengam / utils.py
kargaranamir's picture
Create utils.py
a220ee8
"""
utils for Hengam inference
"""
"""### Import Libraries"""
# import primitive libraries
import os
import pandas as pd
from tqdm import tqdm
import numpy as np
import json
# import seqval to report classifier performance metrics
from seqeval.metrics import accuracy_score, precision_score, recall_score, f1_score
from seqeval.scheme import IOB2
# import torch related modules
import torch
from torch.utils.data import DataLoader
from torch.utils.data import Dataset
from torch.nn.utils.rnn import pad_sequence
import torch.nn as nn
# import pytorch lightning library
import pytorch_lightning as pl
from torchcrf import CRF as SUPERCRF
# import NLTK to create better tokenizer
import nltk
from nltk.tokenize import RegexpTokenizer
# Transformers : Roberta Model
from transformers import XLMRobertaTokenizerFast
from transformers import XLMRobertaModel, XLMRobertaConfig
# import Typings
from typing import Union, Dict, List, Tuple, Any, Optional
import glob
# for sent tokenizer (nltk)
nltk.download('punkt')
"""## XLM-Roberta
### TokenFromSubtoken
- Code adapted from the following [file](https://github.com/deepmipt/DeepPavlov/blob/master/deeppavlov/models/torch_bert/torch_transformers_sequence_tagger.py)
- DeepPavlov is an popular open source library for deep learning end-to-end dialog systems and chatbots.
- Licensed under the Apache License, Version 2.0 (the "License");
"""
class TokenFromSubtoken(torch.nn.Module):
def forward(self, units: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
""" Assemble token level units from subtoken level units
Args:
units: torch.Tensor of shape [batch_size, SUBTOKEN_seq_length, n_features]
mask: mask of token beginnings. For example: for tokens
[[``[CLS]`` ``My``, ``capybara``, ``[SEP]``],
[``[CLS]`` ``Your``, ``aar``, ``##dvark``, ``is``, ``awesome``, ``[SEP]``]]
the mask will be
[[0, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 0, 1, 1, 0]]
Returns:
word_level_units: Units assembled from ones in the mask. For the
example above this units will correspond to the following
[[``My``, ``capybara``],
[``Your`, ``aar``, ``is``, ``awesome``,]]
the shape of this tensor will be [batch_size, TOKEN_seq_length, n_features]
"""
device = units.device
nf_int = units.size()[-1]
batch_size = units.size()[0]
# number of TOKENS in each sentence
token_seq_lengths = torch.sum(mask, 1).to(torch.int64)
# number of words
n_words = torch.sum(token_seq_lengths)
# max token seq len
max_token_seq_len = torch.max(token_seq_lengths)
idxs = torch.stack(torch.nonzero(mask, as_tuple=True), dim=1)
# padding is for computing change from one sample to another in the batch
sample_ids_in_batch = torch.nn.functional.pad(input=idxs[:, 0], pad=[1, 0])
a = (~torch.eq(sample_ids_in_batch[1:], sample_ids_in_batch[:-1])).to(torch.int64)
# transforming sample start masks to the sample starts themselves
q = a * torch.arange(n_words, device=device).to(torch.int64)
count_to_substract = torch.nn.functional.pad(torch.masked_select(q, q.to(torch.bool)), [1, 0])
new_word_indices = torch.arange(n_words, device=device).to(torch.int64) - count_to_substract[torch.cumsum(a, 0)]
n_total_word_elements = max_token_seq_len*torch.ones_like(token_seq_lengths, device=device).sum()
word_indices_flat = (idxs[:, 0] * max_token_seq_len + new_word_indices).to(torch.int64)
#x_mask = torch.sum(torch.nn.functional.one_hot(word_indices_flat, n_total_word_elements), 0)
#x_mask = x_mask.to(torch.bool)
x_mask = torch.zeros(n_total_word_elements, dtype=torch.bool, device=device)
x_mask[word_indices_flat] = torch.ones_like(word_indices_flat, device=device, dtype=torch.bool)
# to get absolute indices we add max_token_seq_len:
# idxs[:, 0] * max_token_seq_len -> [0, 0, 0, 1, 1, 2] * 2 = [0, 0, 0, 3, 3, 6]
# word_indices_flat -> [0, 0, 0, 3, 3, 6] + [0, 1, 2, 0, 1, 0] = [0, 1, 2, 3, 4, 6]
# total number of words in the batch (including paddings)
# batch_size * max_token_seq_len -> 3 * 3 = 9
# tf.one_hot(...) ->
# [[1. 0. 0. 0. 0. 0. 0. 0. 0.]
# [0. 1. 0. 0. 0. 0. 0. 0. 0.]
# [0. 0. 1. 0. 0. 0. 0. 0. 0.]
# [0. 0. 0. 1. 0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 1. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0. 0. 1. 0. 0.]]
# x_mask -> [1, 1, 1, 1, 1, 0, 1, 0, 0]
nonword_indices_flat = (~x_mask).nonzero().squeeze(-1)
# get a sequence of units corresponding to the start subtokens of the words
# size: [n_words, n_features]
elements = units[mask.bool()]
# prepare zeros for paddings
# size: [batch_size * TOKEN_seq_length - n_words, n_features]
paddings = torch.zeros_like(nonword_indices_flat, dtype=elements.dtype).unsqueeze(-1).repeat(1,nf_int).to(device)
# tensor_flat -> [x, x, x, x, x, 0, x, 0, 0]
tensor_flat_unordered = torch.cat([elements, paddings])
_, order_idx = torch.sort(torch.cat([word_indices_flat, nonword_indices_flat]))
tensor_flat = tensor_flat_unordered[order_idx]
tensor = torch.reshape(tensor_flat, (-1, max_token_seq_len, nf_int))
# tensor -> [[x, x, x],
# [x, x, 0],
# [x, 0, 0]]
return tensor
"""### Conditional Random Field
- Code adopted form [torchcrf library](https://pytorch-crf.readthedocs.io/en/stable/)
- we override veiterbi decoder in order to make it compatible with our code
"""
class CRF(SUPERCRF):
# override veiterbi decoder in order to make it compatible with our code
def _viterbi_decode(self, emissions: torch.FloatTensor,
mask: torch.ByteTensor) -> List[List[int]]:
# emissions: (seq_length, batch_size, num_tags)
# mask: (seq_length, batch_size)
assert emissions.dim() == 3 and mask.dim() == 2
assert emissions.shape[:2] == mask.shape
assert emissions.size(2) == self.num_tags
assert mask[0].all()
seq_length, batch_size = mask.shape
# Start transition and first emission
# shape: (batch_size, num_tags)
score = self.start_transitions + emissions[0]
history = []
# score is a tensor of size (batch_size, num_tags) where for every batch,
# value at column j stores the score of the best tag sequence so far that ends
# with tag j
# history saves where the best tags candidate transitioned from; this is used
# when we trace back the best tag sequence
# Viterbi algorithm recursive case: we compute the score of the best tag sequence
# for every possible next tag
for i in range(1, seq_length):
# Broadcast viterbi score for every possible next tag
# shape: (batch_size, num_tags, 1)
broadcast_score = score.unsqueeze(2)
# Broadcast emission score for every possible current tag
# shape: (batch_size, 1, num_tags)
broadcast_emission = emissions[i].unsqueeze(1)
# Compute the score tensor of size (batch_size, num_tags, num_tags) where
# for each sample, entry at row i and column j stores the score of the best
# tag sequence so far that ends with transitioning from tag i to tag j and emitting
# shape: (batch_size, num_tags, num_tags)
next_score = broadcast_score + self.transitions + broadcast_emission
# Find the maximum score over all possible current tag
# shape: (batch_size, num_tags)
next_score, indices = next_score.max(dim=1)
# Set score to the next score if this timestep is valid (mask == 1)
# and save the index that produces the next score
# shape: (batch_size, num_tags)
score = torch.where(mask[i].unsqueeze(1), next_score, score)
history.append(indices)
history = torch.stack(history, dim=0)
# End transition score
# shape: (batch_size, num_tags)
score += self.end_transitions
# Now, compute the best path for each sample
# shape: (batch_size,)
seq_ends = mask.long().sum(dim=0) - 1
best_tags_list = []
for idx in range(batch_size):
# Find the tag which maximizes the score at the last timestep; this is our best tag
# for the last timestep
_, best_last_tag = score[idx].max(dim=0)
best_tags = [best_last_tag]
# We trace back where the best last tag comes from, append that to our best tag
# sequence, and trace it back again, and so on
for i, hist in enumerate(torch.flip(history[:seq_ends[idx]], dims=(0,))):
best_last_tag = hist[idx][best_tags[-1]]
best_tags.append(best_last_tag)
best_tags = torch.stack(best_tags, dim=0)
# Reverse the order because we start from the last timestep
best_tags_list.append(torch.flip(best_tags, dims=(0,)))
best_tags_list = nn.utils.rnn.pad_sequence(best_tags_list, batch_first=True, padding_value=0)
return best_tags_list
"""### CRFLayer
- Forward: decide output logits basaed on backbone network
- Decode: decode based on CRF weights
"""
class CRFLayer(nn.Module):
def __init__(self, embedding_size, n_labels):
super(CRFLayer, self).__init__()
self.dropout = nn.Dropout(0.1)
self.output_dense = nn.Linear(embedding_size,n_labels)
self.crf = CRF(n_labels, batch_first=True)
self.token_from_subtoken = TokenFromSubtoken()
# Forward: decide output logits basaed on backbone network
def forward(self, embedding, mask):
logits = self.output_dense(self.dropout(embedding))
logits = self.token_from_subtoken(logits, mask)
pad_mask = self.token_from_subtoken(mask.unsqueeze(-1), mask).squeeze(-1).bool()
return logits, pad_mask
# Decode: decode based on CRF weights
def decode(self, logits, pad_mask):
return self.crf.decode(logits, pad_mask)
# Evaluation Loss: calculate mean log likelihood of CRF layer
def eval_loss(self, logits, targets, pad_mask):
mean_log_likelihood = self.crf(logits, targets, pad_mask, reduction='sum').mean()
return -mean_log_likelihood
"""### NERModel
- Roberta Model with CRF Layer
"""
class NERModel(nn.Module):
def __init__(self, n_labels:int, roberta_path:str):
super(NERModel,self).__init__()
self.roberta = XLMRobertaModel.from_pretrained(roberta_path)
self.crf = CRFLayer(self.roberta.config.hidden_size, n_labels)
# Forward: pass embedings to CRF layer in order to evaluate logits from suboword sequence
def forward(self,
input_ids:torch.Tensor,
attention_mask:torch.Tensor,
token_type_ids:torch.Tensor,
mask:torch.Tensor) -> torch.Tensor:
embedding = self.roberta(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids)[0]
logits, pad_mask = self.crf(embedding, mask)
return logits, pad_mask
# Disable Gradient and Predict with model
@torch.no_grad()
def predict(self, inputs:Tuple[torch.Tensor]) -> torch.Tensor:
input_ids, attention_mask, token_type_ids, mask = inputs
logits, pad_mask = self(input_ids, attention_mask, token_type_ids, mask)
decoded = self.crf.decode(logits, pad_mask)
return decoded, pad_mask
# Decode: pass to crf decoder and decode based on CRF weights
def decode(self, logits, pad_mask):
"""Decode logits using CRF weights
"""
return self.crf.decode(logits, pad_mask)
# Evaluation Loss: pass to crf eval_loss and calculate mean log likelihood of CRF layer
def eval_loss(self, logits, targets, pad_mask):
return self.crf.eval_loss(logits, targets, pad_mask)
# Determine number of layers to be fine-tuned (!freeze)
def freeze_roberta(self, n_freeze:int=6):
for param in self.roberta.parameters():
param.requires_grad = False
for param in self.roberta.encoder.layer[n_freeze:].parameters():
param.requires_grad = True
"""### NERTokenizer
- NLTK tokenizer along with XLMRobertaTokenizerFast tokenizer
- Code adapted from the following [file](https://github.com/ugurcanozalp/multilingual-ner/blob/main/multiner/utils/custom_tokenizer.py)
"""
class NERTokenizer(object):
MAX_LEN=512
BATCH_LENGTH_LIMT = 380 # Max number of roberta tokens in one sentence.
# Modified version of http://stackoverflow.com/questions/36353125/nltk-regular-expression-tokenizer
PATTERN = r'''(?x) # set flag to allow verbose regexps
(?:[A-Z]\.)+ # abbreviations, e.g. U.S.A. or U.S.A #
| (?:\d+\.) # numbers
| \w+(?:[-.]\w+)* # words with optional internal hyphens
| \$?\d+(?:.\d+)?%? # currency and percentages, e.g. $12.40, 82%
| \.\.\. # ellipsis, and special chars below, includes ], [
| [-\]\[.؟،؛;"'?,():_`“”/°º‘’″…#$%()*+<>=@\\^_{}|~❑&§\!]
| \u200c
'''
def __init__(self, base_model:str, to_device:str='cpu'):
super(NERTokenizer,self).__init__()
self.roberta_tokenizer = XLMRobertaTokenizerFast.from_pretrained(base_model, do_lower_case=False, padding=True, truncation=True)
self.to_device = to_device
self.word_tokenizer = RegexpTokenizer(self.PATTERN)
self.sent_tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
# tokenize batch of tokens
def tokenize_batch(self, inputs, pad_to = None) -> torch.Tensor:
batch = [inputs] if isinstance(inputs[0], str) else inputs
input_ids, attention_mask, token_type_ids, mask = [], [], [], []
for tokens in batch:
input_ids_tmp, attention_mask_tmp, token_type_ids_tmp, mask_tmp = self._tokenize_words(tokens)
input_ids.append(input_ids_tmp)
attention_mask.append(attention_mask_tmp)
token_type_ids.append(token_type_ids_tmp)
mask.append(mask_tmp)
input_ids = pad_sequence(input_ids, batch_first=True, padding_value=self.roberta_tokenizer.pad_token_id)
attention_mask = pad_sequence(attention_mask, batch_first=True, padding_value=0)
token_type_ids = pad_sequence(token_type_ids, batch_first=True, padding_value=0)
mask = pad_sequence(mask, batch_first=True, padding_value=0)
# truncate MAX_LEN
if input_ids.shape[-1]>self.MAX_LEN:
input_ids = input_ids[:,:,:self.MAX_LEN]
attention_mask = attention_mask[:,:,:self.MAX_LEN]
token_type_ids = token_type_ids[:,:,:self.MAX_LEN]
mask = mask[:,:,:self.MAX_LEN]
# extend pad
elif pad_to is not None and pad_to>input_ids.shape[1]:
bs = input_ids.shape[0]
padlen = pad_to-input_ids.shape[1]
input_ids_append = torch.tensor([self.roberta_tokenizer.pad_token_id], dtype=torch.long).repeat([bs, padlen]).to(self.to_device)
input_ids = torch.cat([input_ids, input_ids_append], dim=-1)
attention_mask_append = torch.tensor([0], dtype=torch.long).repeat([bs, padlen]).to(self.to_device)
attention_mask = torch.cat([attention_mask, attention_mask_append], dim=-1)
token_type_ids_append = torch.tensor([0], dtype=torch.long).repeat([bs, padlen]).to(self.to_device)
token_type_ids = torch.cat([token_type_ids, token_type_ids_append], dim=-1)
mask_append = torch.tensor([0], dtype=torch.long).repeat([bs, padlen]).to(self.to_device)
mask = torch.cat([mask, mask_append], dim=-1)
# truncate pad
elif pad_to is not None and pad_to<input_ids.shape[1]:
input_ids = input_ids[:,:,:pad_to]
attention_mask = attention_mask[:,:,:pad_to]
token_type_ids = token_type_ids[:,:,:pad_to]
mask = mask[:,:,:pad_to]
if isinstance(inputs[0], str):
return input_ids[0], attention_mask[0], token_type_ids[0], mask[0]
else:
return input_ids, attention_mask, token_type_ids, mask
# tokenize list of words with roberta tokenizer
def _tokenize_words(self, words):
roberta_tokens = []
mask = []
for word in words:
subtokens = self.roberta_tokenizer.tokenize(word)
roberta_tokens+=subtokens
n_subtoken = len(subtokens)
if n_subtoken>=1:
mask = mask + [1] + [0]*(n_subtoken-1)
# add special tokens [CLS] and [SeP]
roberta_tokens = [self.roberta_tokenizer.cls_token] + roberta_tokens + [self.roberta_tokenizer.sep_token]
mask = [0] + mask + [0]
input_ids = torch.tensor(self.roberta_tokenizer.convert_tokens_to_ids(roberta_tokens), dtype=torch.long).to(self.to_device)
attention_mask = torch.ones(len(mask), dtype=torch.long).to(self.to_device)
token_type_ids = torch.zeros(len(mask), dtype=torch.long).to(self.to_device)
mask = torch.tensor(mask, dtype=torch.long).to(self.to_device)
return input_ids, attention_mask, token_type_ids, mask
# sent_to_token: yield each sentence token with positional span using nltk
def sent_to_token(self, raw_text):
for offset, ending in self.sent_tokenizer.span_tokenize(raw_text):
sub_text = raw_text[offset:ending]
words, spans = [], []
flush = False
total_subtoken = 0
for start, end in self.word_tokenizer.span_tokenize(sub_text):
flush = True
start += offset
end += offset
words.append(raw_text[start:end])
spans.append((start,end))
total_subtoken += len(self.roberta_tokenizer.tokenize(words[-1]))
if (total_subtoken > self.BATCH_LENGTH_LIMT):
# Print
yield words[:-1],spans[:-1]
spans = spans[len(spans)-1:]
words = words[len(words)-1:]
total_subtoken = sum([len(self.roberta_tokenizer.tokenize(word)) for word in words])
flush = False
if flush and len(spans) > 0:
yield words,spans
# Extract (batch words span() from a raw sentence
def prepare_row_text(self, raw_text, batch_size=16):
words_list, spans_list = [], []
end_batch = False
for words, spans in self.sent_to_token(raw_text):
end_batch = True
words_list.append(words)
spans_list.append(spans)
if len(spans_list) >= batch_size:
input_ids, attention_mask, token_type_ids, mask = self.tokenize_batch(words_list)
yield (input_ids, attention_mask, token_type_ids, mask), words_list, spans_list
words_list, spans_list = [], []
if end_batch and len(words_list) > 0:
input_ids, attention_mask, token_type_ids, mask = self.tokenize_batch(words_list)
yield (input_ids, attention_mask, token_type_ids, mask), words_list, spans_list
"""### NER
NER Interface : We Use this interface to infer sentence Time-Date tags.
"""
class NER(object):
def __init__(self, model_path, tags):
self.tags = tags
self.device = "cuda" if torch.cuda.is_available() else "cpu"
# Load Pre-Trained model
roberta_path = "xlm-roberta-base"
self.model = NERModel(n_labels=len(self.tags), roberta_path=roberta_path).to(self.device)
# Load Fine-Tuned model
state_dict = torch.load(model_path)
self.model.load_state_dict(state_dict, strict=False)
# Enable Evaluation mode
self.model.eval()
self.tokenizer = NERTokenizer(base_model=roberta_path, to_device=self.device)
# Predict and Pre/Post-Process the input/output
@torch.no_grad()
def __call__(self, raw_text):
outputs_flat, spans_flat, entities = [], [], []
for batch, words, spans in self.tokenizer.prepare_row_text(raw_text):
output, pad_mask = self.model.predict(batch)
outputs_flat.extend(output[pad_mask.bool()].reshape(-1).tolist())
spans_flat += sum(spans, [])
for tag_idx,(start,end) in zip(outputs_flat,spans_flat):
tag = self.tags[tag_idx]
# filter out O tags
if tag != 'O':
entities.append({'Text': raw_text[start:end],
'Tag': tag,
'Start':start,
'End': end})
return entities