Views
No views yet
1pip install torch transformers datasets nltk
2python -m nltk.downloader punkt1import nltk
2import torch
3import numpy as np
4from datasets import Dataset
5from functools import partial
6from torch.utils.data import DataLoader
7from dataclasses import dataclass, field
8from typing import Any, Dict, List, Union
9from torch.nn.utils.rnn import pad_sequence
10from transformers import AutoTokenizer, BertModel, BertPreTrainedModel
11import torch.nn as nn
12
13class BertTaggerForSentenceExtractionWithBackoff(BertPreTrainedModel):
14 """Sentence-level BERT classifier with a confidence-backoff rule."""
15
16 def __init__(self, config):
17 super().__init__(config)
18 self.num_labels = config.num_labels
19
20 self.bert = BertModel(config)
21 self.dropout = nn.Dropout(config.hidden_dropout_prob)
22 self.classifier = nn.Linear(config.hidden_size, self.num_labels)
23 self.init_weights()
24
25 def forward(
26 self,
27 input_ids=None,
28 attention_mask=None,
29 token_type_ids=None,
30 sentence_ids=None,
31 ):
32 outputs = self.bert(
33 input_ids,
34 attention_mask=attention_mask,
35 token_type_ids=token_type_ids,
36 )
37
38 sequence_output = self.dropout(outputs[0])
39
40 def _get_agg_output(ids, seq_out):
41 max_sentences = torch.max(ids) + 1
42 d_model = seq_out.size(-1)
43
44 agg_out, global_offsets, num_sents = [], [], []
45 for i, sen_ids in enumerate(ids):
46 out, local_ids = [], sen_ids.clone()
47 mask = local_ids != -100
48 offset = local_ids[mask].min()
49 global_offsets.append(offset)
50 local_ids[mask] -= offset
51 n_sent = local_ids.max() + 1
52 num_sents.append(n_sent)
53
54 for j in range(int(n_sent)):
55 out.append(seq_out[i, local_ids == j].mean(dim=-2, keepdim=True))
56
57 if max_sentences - n_sent:
58 padding = torch.zeros(
59 (int(max_sentences - n_sent), d_model), device=seq_out.device
60 )
61 out.append(padding)
62 agg_out.append(torch.cat(out, dim=0))
63 return torch.stack(agg_out), global_offsets, num_sents
64
65 agg_output, offsets, num_sents_item = _get_agg_output(sentence_ids, sequence_output)
66 logits = self.classifier(agg_output)
67 probs = torch.softmax(logits, dim=-1)[:, :, 1]
68
69 def _get_preds(pp, offs, num_s, threshold=0.5, alpha=0.05):
70 preds = []
71 for p, off, ns in zip(pp, offs, num_s):
72 rel_probs = p[:ns]
73 hits = (rel_probs >= threshold).int()
74 if hits.sum() == 0 and rel_probs.max().item() >= alpha:
75 hits[rel_probs.argmax()] = 1
76 preds.append(torch.where(hits == 1)[0] + off)
77 return preds
78
79 return tuple(_get_preds(probs, offsets, num_sents_item))
80
81
82# Dataclass for padding collator
83@dataclass
84class DataCollatorWithPadding:
85 pad_kvs: Dict[str, Union[int, float]] = field(default_factory=dict)
86
87 def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, Any]:
88 first = features[0]
89 batch = {}
90
91 # pad and collate keys in self.pad_kvs
92 for key, pad_value in self.pad_kvs.items():
93 if key in first and first[key] is not None:
94 batch[key] = pad_sequence(
95 [torch.tensor(f[key]) for f in features],
96 batch_first=True,
97 padding_value=pad_value,
98 )
99
100 # collate remaining keys assuming that the values can be stacked
101 for k, v in first.items():
102 if k not in self.pad_kvs and v is not None and isinstance(v, torch.Tensor):
103 batch[k] = torch.stack([f[k] for f in features])
104
105 return batch
106
107
108def prepare_input_features(
109 tokenizer, examples, max_seq_length=510, stride=128, padding=False
110):
111
112 # jointly tokenize questions and context
113 tokenized_examples = tokenizer(
114 examples["question"],
115 examples["context"],
116 truncation="only_second",
117 max_length=max_seq_length,
118 stride=stride,
119 return_overflowing_tokens=True,
120 padding=padding,
121 is_split_into_words=True,
122 )
123
124 sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping")
125 tokenized_examples["example_id"] = []
126 tokenized_examples["word_ids"] = []
127 tokenized_examples["sentence_ids"] = []
128
129 # process model inputs
130 for i, sample_index in enumerate(sample_mapping):
131 word_ids = tokenized_examples.word_ids(i)
132 word_level_sentence_ids = examples["word_level_sentence_ids"][sample_index]
133
134 sequence_ids = tokenized_examples.sequence_ids(i)
135 token_start_index = 0
136 while sequence_ids[token_start_index] != 1:
137 token_start_index += 1
138
139 sentences_ids = [-100] * token_start_index
140 for word_idx in word_ids[token_start_index:]:
141 if word_idx is not None:
142 sentences_ids.append(word_level_sentence_ids[word_idx])
143 else:
144 sentences_ids.append(-100)
145
146 tokenized_examples["sentence_ids"].append(sentences_ids)
147 tokenized_examples["example_id"].append(examples["id"][sample_index])
148 tokenized_examples["word_ids"].append(word_ids)
149
150 # ensure we don't exceed the model's max position embeddings (512 for BERT)
151 for key in ("input_ids", "token_type_ids", "attention_mask", "sentence_ids"):
152 tokenized_examples[key] = [seq[:max_seq_length] for seq in tokenized_examples[key]]
153
154 return tokenized_examples
155
156
157# single example (same as README)
158query = "When does OpenSearch use text reanalysis for highlighting?"
159document = "To highlight the search terms, the highlighter needs the start and end character offsets of each term. The offsets mark the term's position in the original text. The highlighter can obtain the offsets from the following sources: Postings: When documents are indexed, OpenSearch creates an inverted search index—a core data structure used to search for documents. Postings represent the inverted search index and store the mapping of each analyzed term to the list of documents in which it occurs. If you set the index_options parameter to offsets when mapping a text field, OpenSearch adds each term's start and end character offsets to the inverted index. During highlighting, the highlighter reruns the original query directly on the postings to locate each term. Thus, storing offsets makes highlighting more efficient for large fields because it does not require reanalyzing the text. Storing term offsets requires additional disk space, but uses less disk space than storing term vectors. Text reanalysis: In the absence of both postings and term vectors, the highlighter reanalyzes text in order to highlight it. For every document and every field that needs highlighting, the highlighter creates a small in-memory index and reruns the original query through Lucene's query execution planner to access low-level match information for the current document. Reanalyzing the text works well in most use cases. However, this method is more memory and time intensive for large fields."
160
161doc_sents = nltk.sent_tokenize(document)
162sentence_ids, context = [], []
163for sid, sent in enumerate(doc_sents):
164 words = sent.split()
165 context.extend(words)
166 sentence_ids.extend([sid] * len(words))
167
168example_dataset = Dataset.from_dict(
169 {
170 "question": [[query]],
171 "context": [context],
172 "word_level_sentence_ids": [sentence_ids],
173 "id": [0],
174 }
175)
176
177# prepare to featurize the raw text data
178base_model_id = "bert-base-uncased"
179tokenizer = AutoTokenizer.from_pretrained(base_model_id)
180collator = DataCollatorWithPadding(
181 pad_kvs={
182 "input_ids": 0,
183 "token_type_ids": 0,
184 "attention_mask": 0,
185 "sentence_ids": -100,
186 "sentence_labels": -100,
187 }
188 )
189preprocess_fn = partial(prepare_input_features, tokenizer)
190
191# featurize
192example_dataset = example_dataset.map(
193 preprocess_fn,
194 batched=True,
195 remove_columns=example_dataset.column_names,
196 desc="Preparing model inputs",
197)
198loader = DataLoader(example_dataset, batch_size=1, collate_fn=collator)
199
200# get single batch
201batch = next(iter(loader))
202
203# load model and get sentence highlights
204model = BertTaggerForSentenceExtractionWithBackoff.from_pretrained(
205 "opensearch-project/opensearch-semantic-highlighter-v1"
206)
207
208# clamp tensors to model max length
209max_len = model.config.max_position_embeddings
210for key in ("input_ids", "token_type_ids", "attention_mask", "sentence_ids"):
211 batch[key] = batch[key][:, :max_len]
212
213highlights = model(
214 batch["input_ids"],
215 batch["attention_mask"],
216 batch["token_type_ids"],
217 batch["sentence_ids"],
218)
219
220highlighted_sentences = [doc_sents[i] for i in highlights[0]]
221print(highlighted_sentences)