welcomyou commited on
Commit
a7754d0
·
verified ·
1 Parent(s): 154a097

Initial upload: ViBERT-capu ONNX (FP32 + INT8) — converted from dragonSwing/vibert-capu

Browse files
README.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-sa-4.0
3
+ language:
4
+ - vi
5
+ base_model: dragonSwing/vibert-capu
6
+ tags:
7
+ - punctuation
8
+ - capitalization
9
+ - vietnamese
10
+ - onnx
11
+ - bert
12
+ - vibert
13
+ library_name: onnxruntime
14
+ ---
15
+
16
+ # ViBERT-capu ONNX (FP32 + INT8)
17
+
18
+ Vietnamese **punctuation restoration + capitalization** model — ONNX Runtime version of [dragonSwing/vibert-capu](https://huggingface.co/dragonSwing/vibert-capu).
19
+ PyTorch dependency removed (~2 GB → ~50 MB onnxruntime).
20
+
21
+ | Variant | File | Size | Use case |
22
+ |---|---|---|---|
23
+ | FP32 | `vibert-capu.onnx` | 438 MB | Best accuracy, server / web service |
24
+ | INT8 | `vibert-capu.int8.onnx` | 110 MB | Desktop, embedded — dynamic-quantized weights, ~99% of FP32 accuracy |
25
+
26
+ Architecture: BERT (FPTAI/vibert-base-cased) fine-tuned by [dragonSwing](https://huggingface.co/dragonSwing) on 5.6M OSCAR-2109 samples for the Seq2Labels punctuation+capitalization task (15 GECToR-style edit actions).
27
+
28
+ ## Why ONNX?
29
+
30
+ | | PyTorch (original) | ONNX Runtime (this repo) |
31
+ |---|---|---|
32
+ | Cold start | ~6 s | ~0.8 s |
33
+ | Runtime deps | torch (~2 GB) | onnxruntime (~50 MB) |
34
+ | Portable build | very heavy | lightweight |
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ import numpy as np
40
+ import onnxruntime as ort
41
+ from huggingface_hub import snapshot_download
42
+ from transformers import AutoTokenizer
43
+
44
+ local = snapshot_download("welcomyou/vibert-capu-onnx")
45
+ tok = AutoTokenizer.from_pretrained(local)
46
+ sess = ort.InferenceSession(f"{local}/vibert-capu.int8.onnx",
47
+ providers=["CPUExecutionProvider"])
48
+
49
+ text = "hà nội là thủ đô việt nam tôi yêu nó"
50
+ enc = tok(text.split(), is_split_into_words=True, return_tensors="np")
51
+ # input_offsets: index of first subword for each word
52
+ word_ids = enc.word_ids()
53
+ offsets = []
54
+ prev = None
55
+ for i, w in enumerate(word_ids):
56
+ if w is not None and w != prev:
57
+ offsets.append(i); prev = w
58
+ input_offsets = np.array([offsets], dtype=np.int64)
59
+
60
+ logits, detect_logits = sess.run(None, {
61
+ "input_ids": enc["input_ids"].astype(np.int64),
62
+ "attention_mask": enc["attention_mask"].astype(np.int64),
63
+ "token_type_ids": enc["token_type_ids"].astype(np.int64),
64
+ "input_offsets": input_offsets,
65
+ })
66
+ # logits: (1, num_words, 15) — 15 GECToR actions
67
+ # detect_logits: (1, num_words, 4) — error detection
68
+ ```
69
+
70
+ ## Model I/O
71
+
72
+ **Inputs** (all `int64`):
73
+
74
+ | Name | Shape | Description |
75
+ |---|---|---|
76
+ | `input_ids` | `(batch, seq_len)` | BPE token IDs from BertTokenizer |
77
+ | `attention_mask` | `(batch, seq_len)` | 1 = real token, 0 = padding |
78
+ | `token_type_ids` | `(batch, seq_len)` | Segment IDs (always 0) |
79
+ | `input_offsets` | `(batch, num_words)` | Index of first subword for each whitespace-separated word |
80
+
81
+ **Outputs** (`float32`):
82
+
83
+ | Name | Shape | Description |
84
+ |---|---|---|
85
+ | `logits` | `(batch, num_words, 15)` | Action probabilities (15 GECToR-style edits) |
86
+ | `detect_logits` | `(batch, num_words, 4)` | Error-detection probabilities |
87
+
88
+ **15 actions:**
89
+
90
+ ```
91
+ $KEEP Giữ nguyên
92
+ $TRANSFORM_CASE_CAPITAL Viết hoa chữ cái đầu (hà nội → Hà Nội)
93
+ $APPEND_, Thêm dấu phẩy
94
+ $APPEND_. Thêm dấu chấm
95
+ $TRANSFORM_VERB_VB_VBN (không dùng cho tiếng Việt)
96
+ $TRANSFORM_CASE_UPPER Viết hoa toàn bộ (who → WHO)
97
+ $APPEND_: Thêm dấu hai chấm
98
+ $APPEND_? Thêm dấu hỏi
99
+ $TRANSFORM_VERB_VB_VBC (không dùng cho tiếng Việt)
100
+ $TRANSFORM_CASE_LOWER Viết thường
101
+ $TRANSFORM_CASE_CAPITAL_1 Viết hoa ký tự thứ 2
102
+ $TRANSFORM_CASE_UPPER_-1 Viết hoa trừ ký tự cuối
103
+ $MERGE_SPACE Nối từ
104
+ @@UNKNOWN@@
105
+ @@PADDING@@
106
+ ```
107
+
108
+ ## Reproducing the export
109
+
110
+ ```bash
111
+ git clone https://huggingface.co/dragonSwing/vibert-capu
112
+ pip install torch transformers onnxruntime numpy
113
+
114
+ # Export FP32 + dynamic-quantize INT8 in one step:
115
+ python convert_onnx/export_vibert_onnx.py \
116
+ --model_dir vibert-capu \
117
+ --output vibert-capu.onnx \
118
+ --opset 14 \
119
+ --verify
120
+ ```
121
+
122
+ Script: [`convert_onnx/export_vibert_onnx.py`](https://github.com/welcomyou/sherpa-vietnamese-asr/blob/main/convert_onnx/export_vibert_onnx.py).
123
+
124
+ ## Files
125
+
126
+ ```
127
+ config.json BERT config (from dragonSwing)
128
+ vocab.txt BERT vocabulary (from dragonSwing)
129
+ vocabulary/ GECToR action labels
130
+ d_tags.txt
131
+ labels.txt
132
+ non_padded_namespaces.txt
133
+ verb-form-vocab.txt Verb form vocabulary
134
+ vibert-capu.onnx FP32 ONNX (438 MB)
135
+ vibert-capu.int8.onnx INT8 ONNX (110 MB)
136
+ configuration_seq2labels.py Seq2Labels HF config class
137
+ modeling_seq2labels.py Seq2Labels HF model class (PyTorch reference, not used at runtime)
138
+ gec_model.py GECToR inference helpers
139
+ utils.py Tokenization helpers
140
+ vocabulary.py GECToR Vocabulary class
141
+ ```
142
+
143
+ ## Credits & License
144
+
145
+ - **Original model**: [dragonSwing/vibert-capu](https://huggingface.co/dragonSwing/vibert-capu)
146
+ - **Base BERT**: [FPTAI/vibert-base-cased](https://huggingface.co/FPTAI/vibert-base-cased)
147
+ - **Training data**: [OSCAR-2109](https://huggingface.co/datasets/oscar-corpus/OSCAR-2109) Vietnamese subset (5.6M samples)
148
+
149
+ License: **CC-BY-SA-4.0** (inherited from dragonSwing/vibert-capu — derivative works must use the same license).
150
+
151
+ ## Used by
152
+
153
+ - [Sherpa Vietnamese ASR](https://github.com/welcomyou/sherpa-vietnamese-asr) — offline Vietnamese ASR for desktop and web (CPU-only).
config.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "attention_probs_dropout_prob": 0.1,
3
+ "bert_vocab_size": 38168,
4
+ "hidden_act": "gelu",
5
+ "hidden_dropout_prob": 0.1,
6
+ "hidden_size": 768,
7
+ "initializer_range": 0.02,
8
+ "intermediate_size": 3072,
9
+ "label_smoothing": 0.0,
10
+ "layer_norm_eps": 1e-12,
11
+ "load_pretrained": false,
12
+ "max_position_embeddings": 512,
13
+ "model_type": "bert",
14
+ "num_attention_heads": 12,
15
+ "num_detect_classes": 4,
16
+ "num_hidden_layers": 12,
17
+ "num_labels": 15,
18
+ "pad_token_id": 0,
19
+ "predictor_dropout": 0.0,
20
+ "pretrained_name_or_path": "FPTAI/vibert-base-cased",
21
+ "special_tokens_fix": true,
22
+ "transformers_version": "4.18.0",
23
+ "type_vocab_size": 2,
24
+ "use_cache": true,
25
+ "vocab_size": 15
26
+ }
configuration_seq2labels.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class Seq2LabelsConfig(PretrainedConfig):
5
+ r"""
6
+ This is the configuration class to store the configuration of a [`Seq2LabelsModel`]. It is used to
7
+ instantiate a Seq2Labels model according to the specified arguments, defining the model architecture. Instantiating a
8
+ configuration with the defaults will yield a similar configuration to that of the Seq2Labels architecture.
9
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
10
+ documentation from [`PretrainedConfig`] for more information.
11
+ Args:
12
+ vocab_size (`int`, *optional*, defaults to 30522):
13
+ Vocabulary size of the BERT model. Defines the number of different tokens that can be represented by the
14
+ `inputs_ids` passed when calling [`BertModel`] or [`TFBertModel`].
15
+ pretrained_name_or_path (`str`, *optional*, defaults to `bert-base-cased`):
16
+ Pretrained BERT-like model path
17
+ load_pretrained (`bool`, *optional*, defaults to `False`):
18
+ Whether to load pretrained model from `pretrained_name_or_path`
19
+ use_cache (`bool`, *optional*, defaults to `True`):
20
+ Whether or not the model should return the last key/values attentions (not used by all models). Only
21
+ relevant if `config.is_decoder=True`.
22
+ predictor_dropout (`float`, *optional*):
23
+ The dropout ratio for the classification head.
24
+ special_tokens_fix (`bool`, *optional*, defaults to `False`):
25
+ Whether to add additional tokens to the BERT's embedding layer.
26
+ Examples:
27
+ ```python
28
+ >>> from transformers import BertModel, BertConfig
29
+ >>> # Initializing a Seq2Labels style configuration
30
+ >>> configuration = Seq2LabelsConfig()
31
+ >>> # Initializing a model from the bert-base-uncased style configuration
32
+ >>> model = Seq2LabelsModel(configuration)
33
+ >>> # Accessing the model configuration
34
+ >>> configuration = model.config
35
+ ```"""
36
+ model_type = "bert"
37
+
38
+ def __init__(
39
+ self,
40
+ pretrained_name_or_path="bert-base-cased",
41
+ vocab_size=15,
42
+ num_detect_classes=4,
43
+ load_pretrained=False,
44
+ initializer_range=0.02,
45
+ pad_token_id=0,
46
+ use_cache=True,
47
+ predictor_dropout=0.0,
48
+ special_tokens_fix=False,
49
+ label_smoothing=0.0,
50
+ **kwargs
51
+ ):
52
+ super().__init__(pad_token_id=pad_token_id, **kwargs)
53
+
54
+ self.vocab_size = vocab_size
55
+ self.num_detect_classes = num_detect_classes
56
+ self.pretrained_name_or_path = pretrained_name_or_path
57
+ self.load_pretrained = load_pretrained
58
+ self.initializer_range = initializer_range
59
+ self.use_cache = use_cache
60
+ self.predictor_dropout = predictor_dropout
61
+ self.special_tokens_fix = special_tokens_fix
62
+ self.label_smoothing = label_smoothing
gec_model.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wrapper of Seq2Labels model. Fixes errors based on model predictions"""
2
+ from collections import defaultdict
3
+ from difflib import SequenceMatcher
4
+ import logging
5
+ import re
6
+ from time import time
7
+ from typing import List, Union
8
+ import warnings
9
+
10
+ import torch
11
+ from transformers import AutoTokenizer
12
+ from modeling_seq2labels import Seq2LabelsModel
13
+ from vocabulary import Vocabulary
14
+ from utils import PAD, UNK, START_TOKEN, get_target_sent_by_edits
15
+
16
+ logging.getLogger("werkzeug").setLevel(logging.ERROR)
17
+ logger = logging.getLogger(__file__)
18
+
19
+
20
+ class GecBERTModel(torch.nn.Module):
21
+ def __init__(
22
+ self,
23
+ vocab_path=None,
24
+ model_paths=None,
25
+ weights=None,
26
+ device=None,
27
+ max_len=64,
28
+ min_len=3,
29
+ lowercase_tokens=False,
30
+ log=False,
31
+ iterations=3,
32
+ min_error_probability=0.0,
33
+ confidence=0,
34
+ resolve_cycles=False,
35
+ split_chunk=False,
36
+ chunk_size=48,
37
+ overlap_size=12,
38
+ min_words_cut=6,
39
+ punc_dict={':', ".", ",", "?"},
40
+ ):
41
+ r"""
42
+ Args:
43
+ vocab_path (`str`):
44
+ Path to vocabulary directory.
45
+ model_paths (`List[str]`):
46
+ List of model paths.
47
+ weights (`int`, *Optional*, defaults to None):
48
+ Weights of each model. Only relevant if `is_ensemble is True`.
49
+ device (`int`, *Optional*, defaults to None):
50
+ Device to load model. If not set, device will be automatically choose.
51
+ max_len (`int`, defaults to 64):
52
+ Max sentence length to be processed (all longer will be truncated).
53
+ min_len (`int`, defaults to 3):
54
+ Min sentence length to be processed (all shorted will be returned w/o changes).
55
+ lowercase_tokens (`bool`, defaults to False):
56
+ Whether to lowercase tokens.
57
+ log (`bool`, defaults to False):
58
+ Whether to enable logging.
59
+ iterations (`int`, defaults to 3):
60
+ Max iterations to run during inference.
61
+ special_tokens_fix (`bool`, defaults to True):
62
+ Whether to fix problem with [CLS], [SEP] tokens tokenization.
63
+ min_error_probability (`float`, defaults to `0.0`):
64
+ Minimum probability for each action to apply.
65
+ confidence (`float`, defaults to `0.0`):
66
+ How many probability to add to $KEEP token.
67
+ split_chunk (`bool`, defaults to False):
68
+ Whether to split long sentences to multiple segments of `chunk_size`.
69
+ !Warning: if `chunk_size > max_len`, each segment will be truncate to `max_len`.
70
+ chunk_size (`int`, defaults to 48):
71
+ Length of each segment (in words). Only relevant if `split_chunk is True`.
72
+ overlap_size (`int`, defaults to 12):
73
+ Overlap size (in words) between two consecutive segments. Only relevant if `split_chunk is True`.
74
+ min_words_cut (`int`, defaults to 6):
75
+ Minimun number of words to be cut while merging two consecutive segments.
76
+ Only relevant if `split_chunk is True`.
77
+ punc_dict (List[str], defaults to `{':', ".", ",", "?"}`):
78
+ List of punctuations.
79
+ """
80
+ super().__init__()
81
+ if isinstance(model_paths, str):
82
+ model_paths = [model_paths]
83
+ self.model_weights = list(map(float, weights)) if weights else [1] * len(model_paths)
84
+ self.device = (
85
+ torch.device("cuda" if torch.cuda.is_available() else "cpu") if device is None else torch.device(device)
86
+ )
87
+ self.max_len = max_len
88
+ self.min_len = min_len
89
+ self.lowercase_tokens = lowercase_tokens
90
+ self.min_error_probability = min_error_probability
91
+ self.vocab = Vocabulary.from_files(vocab_path)
92
+ self.incorr_index = self.vocab.get_token_index("INCORRECT", "d_tags")
93
+ self.log = log
94
+ self.iterations = iterations
95
+ self.confidence = confidence
96
+ self.resolve_cycles = resolve_cycles
97
+ assert (
98
+ chunk_size > 0 and chunk_size // 2 >= overlap_size
99
+ ), "Chunk merging required overlap size must be smaller than half of chunk size"
100
+ self.split_chunk = split_chunk
101
+ self.chunk_size = chunk_size
102
+ self.overlap_size = overlap_size
103
+ self.min_words_cut = min_words_cut
104
+ self.stride = chunk_size - overlap_size
105
+ self.punc_dict = punc_dict
106
+ self.punc_str = '[' + ''.join([f'\{x}' for x in punc_dict]) + ']'
107
+ # set training parameters and operations
108
+
109
+ self.indexers = []
110
+ self.models = []
111
+ for model_path in model_paths:
112
+ model = Seq2LabelsModel.from_pretrained(model_path)
113
+ config = model.config
114
+ model_name = config.pretrained_name_or_path
115
+ special_tokens_fix = config.special_tokens_fix
116
+ self.indexers.append(self._get_indexer(model_name, special_tokens_fix))
117
+ model.eval().to(self.device)
118
+ self.models.append(model)
119
+
120
+ def _get_indexer(self, weights_name, special_tokens_fix):
121
+ tokenizer = AutoTokenizer.from_pretrained(
122
+ weights_name, do_basic_tokenize=False, do_lower_case=self.lowercase_tokens, model_max_length=1024
123
+ )
124
+ # to adjust all tokenizers
125
+ if hasattr(tokenizer, 'encoder'):
126
+ tokenizer.vocab = tokenizer.encoder
127
+ if hasattr(tokenizer, 'sp_model'):
128
+ tokenizer.vocab = defaultdict(lambda: 1)
129
+ for i in range(tokenizer.sp_model.get_piece_size()):
130
+ tokenizer.vocab[tokenizer.sp_model.id_to_piece(i)] = i
131
+
132
+ if special_tokens_fix:
133
+ tokenizer.add_tokens([START_TOKEN])
134
+ tokenizer.vocab[START_TOKEN] = len(tokenizer) - 1
135
+ return tokenizer
136
+
137
+ def forward(self, text: Union[str, List[str], List[List[str]]], is_split_into_words=False):
138
+ # Input type checking for clearer error
139
+ def _is_valid_text_input(t):
140
+ if isinstance(t, str):
141
+ # Strings are fine
142
+ return True
143
+ elif isinstance(t, (list, tuple)):
144
+ # List are fine as long as they are...
145
+ if len(t) == 0:
146
+ # ... empty
147
+ return True
148
+ elif isinstance(t[0], str):
149
+ # ... list of strings
150
+ return True
151
+ elif isinstance(t[0], (list, tuple)):
152
+ # ... list with an empty list or with a list of strings
153
+ return len(t[0]) == 0 or isinstance(t[0][0], str)
154
+ else:
155
+ return False
156
+ else:
157
+ return False
158
+
159
+ if not _is_valid_text_input(text):
160
+ raise ValueError(
161
+ "text input must of type `str` (single example), `List[str]` (batch or single pretokenized example) "
162
+ "or `List[List[str]]` (batch of pretokenized examples)."
163
+ )
164
+
165
+ if is_split_into_words:
166
+ is_batched = isinstance(text, (list, tuple)) and text and isinstance(text[0], (list, tuple))
167
+ else:
168
+ is_batched = isinstance(text, (list, tuple))
169
+ if is_batched:
170
+ text = [x.split() for x in text]
171
+ else:
172
+ text = text.split()
173
+
174
+ if not is_batched:
175
+ text = [text]
176
+
177
+ return self.handle_batch(text)
178
+
179
+ def split_chunks(self, batch):
180
+ # return batch pairs of indices
181
+ result = []
182
+ indices = []
183
+ for tokens in batch:
184
+ start = len(result)
185
+ num_token = len(tokens)
186
+ if num_token <= self.chunk_size:
187
+ result.append(tokens)
188
+ elif num_token > self.chunk_size and num_token < (self.chunk_size * 2 - self.overlap_size):
189
+ split_idx = (num_token + self.overlap_size + 1) // 2
190
+ result.append(tokens[:split_idx])
191
+ result.append(tokens[split_idx - self.overlap_size :])
192
+ else:
193
+ for i in range(0, num_token - self.overlap_size, self.stride):
194
+ result.append(tokens[i : i + self.chunk_size])
195
+
196
+ indices.append((start, len(result)))
197
+
198
+ return result, indices
199
+
200
+ def check_alnum(self, s):
201
+ if len(s) < 2:
202
+ return False
203
+ return not (s.isalpha() or s.isdigit())
204
+
205
+ def apply_chunk_merging(self, tokens, next_tokens):
206
+ # Return next tokens if current tokens list is empty
207
+ if not tokens:
208
+ return next_tokens
209
+
210
+ source_token_idx = []
211
+ target_token_idx = []
212
+ source_tokens = []
213
+ target_tokens = []
214
+ num_keep = self.overlap_size - self.min_words_cut
215
+ i = 0
216
+ while len(source_token_idx) < self.overlap_size and -i < len(tokens):
217
+ i -= 1
218
+ if tokens[i] not in self.punc_dict:
219
+ source_token_idx.insert(0, i)
220
+ source_tokens.insert(0, tokens[i].lower())
221
+
222
+ i = 0
223
+ while len(target_token_idx) < self.overlap_size and i < len(next_tokens):
224
+ if next_tokens[i] not in self.punc_dict:
225
+ target_token_idx.append(i)
226
+ target_tokens.append(next_tokens[i].lower())
227
+ i += 1
228
+
229
+ matcher = SequenceMatcher(None, source_tokens, target_tokens)
230
+ diffs = list(matcher.get_opcodes())
231
+
232
+ for diff in diffs:
233
+ tag, i1, i2, j1, j2 = diff
234
+ if tag == "equal":
235
+ if i1 >= num_keep:
236
+ tail_idx = source_token_idx[i1]
237
+ head_idx = target_token_idx[j1]
238
+ break
239
+ elif i2 > num_keep:
240
+ tail_idx = source_token_idx[num_keep]
241
+ head_idx = target_token_idx[j2 - i2 + num_keep]
242
+ break
243
+ elif tag == "delete" and i1 == 0:
244
+ num_keep += i2 // 2
245
+
246
+ tokens = tokens[:tail_idx] + next_tokens[head_idx:]
247
+ return tokens
248
+
249
+ def merge_chunks(self, batch):
250
+ result = []
251
+ if len(batch) == 1 or self.overlap_size == 0:
252
+ for sub_tokens in batch:
253
+ result.extend(sub_tokens)
254
+ else:
255
+ for _, sub_tokens in enumerate(batch):
256
+ try:
257
+ result = self.apply_chunk_merging(result, sub_tokens)
258
+ except Exception as e:
259
+ print(e)
260
+
261
+ result = " ".join(result)
262
+ return result
263
+
264
+ def predict(self, batches):
265
+ t11 = time()
266
+ predictions = []
267
+ for batch, model in zip(batches, self.models):
268
+ batch = batch.to(self.device)
269
+ with torch.no_grad():
270
+ prediction = model.forward(**batch)
271
+ predictions.append(prediction)
272
+
273
+ preds, idx, error_probs = self._convert(predictions)
274
+ t55 = time()
275
+ if self.log:
276
+ print(f"Inference time {t55 - t11}")
277
+ return preds, idx, error_probs
278
+
279
+ def get_token_action(self, token, index, prob, sugg_token):
280
+ """Get lost of suggested actions for token."""
281
+ # cases when we don't need to do anything
282
+ if prob < self.min_error_probability or sugg_token in [UNK, PAD, '$KEEP']:
283
+ return None
284
+
285
+ if sugg_token.startswith('$REPLACE_') or sugg_token.startswith('$TRANSFORM_') or sugg_token == '$DELETE':
286
+ start_pos = index
287
+ end_pos = index + 1
288
+ elif sugg_token.startswith("$APPEND_") or sugg_token.startswith("$MERGE_"):
289
+ start_pos = index + 1
290
+ end_pos = index + 1
291
+
292
+ if sugg_token == "$DELETE":
293
+ sugg_token_clear = ""
294
+ elif sugg_token.startswith('$TRANSFORM_') or sugg_token.startswith("$MERGE_"):
295
+ sugg_token_clear = sugg_token[:]
296
+ else:
297
+ sugg_token_clear = sugg_token[sugg_token.index('_') + 1 :]
298
+
299
+ return start_pos - 1, end_pos - 1, sugg_token_clear, prob
300
+
301
+ def preprocess(self, token_batch):
302
+ seq_lens = [len(sequence) for sequence in token_batch if sequence]
303
+ if not seq_lens:
304
+ return []
305
+ max_len = min(max(seq_lens), self.max_len)
306
+ batches = []
307
+ for indexer in self.indexers:
308
+ token_batch = [[START_TOKEN] + sequence[:max_len] for sequence in token_batch]
309
+ batch = indexer(
310
+ token_batch,
311
+ return_tensors="pt",
312
+ padding=True,
313
+ is_split_into_words=True,
314
+ truncation=True,
315
+ add_special_tokens=False,
316
+ )
317
+ offset_batch = []
318
+ for i in range(len(token_batch)):
319
+ word_ids = batch.word_ids(batch_index=i)
320
+ offsets = [0]
321
+ for i in range(1, len(word_ids)):
322
+ if word_ids[i] != word_ids[i - 1]:
323
+ offsets.append(i)
324
+ offset_batch.append(torch.LongTensor(offsets))
325
+
326
+ batch["input_offsets"] = torch.nn.utils.rnn.pad_sequence(
327
+ offset_batch, batch_first=True, padding_value=0
328
+ ).to(torch.long)
329
+
330
+ batches.append(batch)
331
+
332
+ return batches
333
+
334
+ def _convert(self, data):
335
+ all_class_probs = torch.zeros_like(data[0]['logits'])
336
+ error_probs = torch.zeros_like(data[0]['max_error_probability'])
337
+ for output, weight in zip(data, self.model_weights):
338
+ class_probabilities_labels = torch.softmax(output['logits'], dim=-1)
339
+ all_class_probs += weight * class_probabilities_labels / sum(self.model_weights)
340
+ class_probabilities_d = torch.softmax(output['detect_logits'], dim=-1)
341
+ error_probs_d = class_probabilities_d[:, :, self.incorr_index]
342
+ incorr_prob = torch.max(error_probs_d, dim=-1)[0]
343
+ error_probs += weight * incorr_prob / sum(self.model_weights)
344
+
345
+ max_vals = torch.max(all_class_probs, dim=-1)
346
+ probs = max_vals[0].tolist()
347
+ idx = max_vals[1].tolist()
348
+ return probs, idx, error_probs.tolist()
349
+
350
+ def update_final_batch(self, final_batch, pred_ids, pred_batch, prev_preds_dict):
351
+ new_pred_ids = []
352
+ total_updated = 0
353
+ for i, orig_id in enumerate(pred_ids):
354
+ orig = final_batch[orig_id]
355
+ pred = pred_batch[i]
356
+ prev_preds = prev_preds_dict[orig_id]
357
+ if orig != pred and pred not in prev_preds:
358
+ final_batch[orig_id] = pred
359
+ new_pred_ids.append(orig_id)
360
+ prev_preds_dict[orig_id].append(pred)
361
+ total_updated += 1
362
+ elif orig != pred and pred in prev_preds:
363
+ # update final batch, but stop iterations
364
+ final_batch[orig_id] = pred
365
+ total_updated += 1
366
+ else:
367
+ continue
368
+ return final_batch, new_pred_ids, total_updated
369
+
370
+ def postprocess_batch(self, batch, all_probabilities, all_idxs, error_probs):
371
+ all_results = []
372
+ noop_index = self.vocab.get_token_index("$KEEP", "labels")
373
+ for tokens, probabilities, idxs, error_prob in zip(batch, all_probabilities, all_idxs, error_probs):
374
+ length = min(len(tokens), self.max_len)
375
+ edits = []
376
+
377
+ # skip whole sentences if there no errors
378
+ if max(idxs) == 0:
379
+ all_results.append(tokens)
380
+ continue
381
+
382
+ # skip whole sentence if probability of correctness is not high
383
+ if error_prob < self.min_error_probability:
384
+ all_results.append(tokens)
385
+ continue
386
+
387
+ for i in range(length + 1):
388
+ # because of START token
389
+ if i == 0:
390
+ token = START_TOKEN
391
+ else:
392
+ token = tokens[i - 1]
393
+ # skip if there is no error
394
+ if idxs[i] == noop_index:
395
+ continue
396
+
397
+ sugg_token = self.vocab.get_token_from_index(idxs[i], namespace='labels')
398
+ action = self.get_token_action(token, i, probabilities[i], sugg_token)
399
+ if not action:
400
+ continue
401
+
402
+ edits.append(action)
403
+ all_results.append(get_target_sent_by_edits(tokens, edits))
404
+ return all_results
405
+
406
+ def handle_batch(self, full_batch, merge_punc=True):
407
+ """
408
+ Handle batch of requests.
409
+ """
410
+ if self.split_chunk:
411
+ full_batch, indices = self.split_chunks(full_batch)
412
+ else:
413
+ indices = None
414
+ final_batch = full_batch[:]
415
+ batch_size = len(full_batch)
416
+ prev_preds_dict = {i: [final_batch[i]] for i in range(len(final_batch))}
417
+ short_ids = [i for i in range(len(full_batch)) if len(full_batch[i]) < self.min_len]
418
+ pred_ids = [i for i in range(len(full_batch)) if i not in short_ids]
419
+ total_updates = 0
420
+
421
+ for n_iter in range(self.iterations):
422
+ orig_batch = [final_batch[i] for i in pred_ids]
423
+
424
+ sequences = self.preprocess(orig_batch)
425
+
426
+ if not sequences:
427
+ break
428
+ probabilities, idxs, error_probs = self.predict(sequences)
429
+
430
+ pred_batch = self.postprocess_batch(orig_batch, probabilities, idxs, error_probs)
431
+ if self.log:
432
+ print(f"Iteration {n_iter + 1}. Predicted {round(100*len(pred_ids)/batch_size, 1)}% of sentences.")
433
+
434
+ final_batch, pred_ids, cnt = self.update_final_batch(final_batch, pred_ids, pred_batch, prev_preds_dict)
435
+ total_updates += cnt
436
+
437
+ if not pred_ids:
438
+ break
439
+ if self.split_chunk:
440
+ final_batch = [self.merge_chunks(final_batch[start:end]) for (start, end) in indices]
441
+ else:
442
+ final_batch = [" ".join(x) for x in final_batch]
443
+ if merge_punc:
444
+ final_batch = [re.sub(r'\s+(%s)' % self.punc_str, r'\1', x) for x in final_batch]
445
+
446
+ return final_batch
modeling_seq2labels.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Optional, Tuple, Union
2
+ from torch import nn
3
+ from torch.nn import CrossEntropyLoss
4
+ from transformers import AutoConfig, AutoModel, BertPreTrainedModel
5
+ from transformers.modeling_outputs import ModelOutput
6
+
7
+ import torch
8
+
9
+
10
+ def get_range_vector(size: int, device: int) -> torch.Tensor:
11
+ """
12
+ Returns a range vector with the desired size, starting at 0. The CUDA implementation
13
+ is meant to avoid copy data from CPU to GPU.
14
+ """
15
+ return torch.arange(0, size, dtype=torch.long, device=device)
16
+
17
+
18
+ class Seq2LabelsOutput(ModelOutput):
19
+ loss: Optional[torch.FloatTensor] = None
20
+ logits: torch.FloatTensor = None
21
+ detect_logits: torch.FloatTensor = None
22
+ hidden_states: Optional[Tuple[torch.FloatTensor]] = None
23
+ attentions: Optional[Tuple[torch.FloatTensor]] = None
24
+ max_error_probability: Optional[torch.FloatTensor] = None
25
+
26
+
27
+ class Seq2LabelsModel(BertPreTrainedModel):
28
+
29
+ _keys_to_ignore_on_load_unexpected = [r"pooler"]
30
+
31
+ def __init__(self, config):
32
+ super().__init__(config)
33
+ self.num_labels = config.num_labels
34
+ self.num_detect_classes = config.num_detect_classes
35
+ self.label_smoothing = config.label_smoothing
36
+
37
+ if config.load_pretrained:
38
+ self.bert = AutoModel.from_pretrained(config.pretrained_name_or_path)
39
+ bert_config = self.bert.config
40
+ else:
41
+ # Uu tien dung config hien tai (da co cac truong BERT architecture)
42
+ # thay vi goi AutoConfig.from_pretrained() online
43
+ if hasattr(config, 'hidden_size') and config.hidden_size:
44
+ from copy import deepcopy
45
+ bert_config = deepcopy(config)
46
+ # vocab_size trong config la so labels (15), khong phai BERT tokenizer vocab
47
+ bert_config.vocab_size = getattr(config, 'bert_vocab_size', 38168)
48
+ else:
49
+ bert_config = AutoConfig.from_pretrained(config.pretrained_name_or_path)
50
+ self.bert = AutoModel.from_config(bert_config)
51
+
52
+ if config.special_tokens_fix:
53
+ try:
54
+ vocab_size = self.bert.embeddings.word_embeddings.num_embeddings
55
+ except AttributeError:
56
+ # reserve more space
57
+ vocab_size = self.bert.word_embedding.num_embeddings + 5
58
+ self.bert.resize_token_embeddings(vocab_size + 1)
59
+
60
+ predictor_dropout = config.predictor_dropout if config.predictor_dropout is not None else 0.0
61
+ self.dropout = nn.Dropout(predictor_dropout)
62
+ self.classifier = nn.Linear(bert_config.hidden_size, config.vocab_size)
63
+ self.detector = nn.Linear(bert_config.hidden_size, config.num_detect_classes)
64
+
65
+ # Initialize weights and apply final processing
66
+ self.post_init()
67
+
68
+ def forward(
69
+ self,
70
+ input_ids: Optional[torch.Tensor] = None,
71
+ input_offsets: Optional[torch.Tensor] = None,
72
+ attention_mask: Optional[torch.Tensor] = None,
73
+ token_type_ids: Optional[torch.Tensor] = None,
74
+ position_ids: Optional[torch.Tensor] = None,
75
+ head_mask: Optional[torch.Tensor] = None,
76
+ inputs_embeds: Optional[torch.Tensor] = None,
77
+ labels: Optional[torch.Tensor] = None,
78
+ d_tags: Optional[torch.Tensor] = None,
79
+ output_attentions: Optional[bool] = None,
80
+ output_hidden_states: Optional[bool] = None,
81
+ return_dict: Optional[bool] = None,
82
+ ) -> Union[Tuple[torch.Tensor], Seq2LabelsOutput]:
83
+ r"""
84
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
85
+ Labels for computing the token classification loss. Indices should be in `[0, ..., config.num_labels - 1]`.
86
+ """
87
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
88
+
89
+ outputs = self.bert(
90
+ input_ids,
91
+ attention_mask=attention_mask,
92
+ token_type_ids=token_type_ids,
93
+ position_ids=position_ids,
94
+ head_mask=head_mask,
95
+ inputs_embeds=inputs_embeds,
96
+ output_attentions=output_attentions,
97
+ output_hidden_states=output_hidden_states,
98
+ return_dict=return_dict,
99
+ )
100
+
101
+ sequence_output = outputs[0]
102
+
103
+ if input_offsets is not None:
104
+ # offsets is (batch_size, d1, ..., dn, orig_sequence_length)
105
+ range_vector = get_range_vector(input_offsets.size(0), device=sequence_output.device).unsqueeze(1)
106
+ # selected embeddings is also (batch_size * d1 * ... * dn, orig_sequence_length)
107
+ sequence_output = sequence_output[range_vector, input_offsets]
108
+
109
+ logits = self.classifier(self.dropout(sequence_output))
110
+ logits_d = self.detector(sequence_output)
111
+
112
+ loss = None
113
+ if labels is not None and d_tags is not None:
114
+ loss_labels_fct = CrossEntropyLoss(label_smoothing=self.label_smoothing)
115
+ loss_d_fct = CrossEntropyLoss()
116
+ loss_labels = loss_labels_fct(logits.view(-1, self.num_labels), labels.view(-1))
117
+ loss_d = loss_d_fct(logits_d.view(-1, self.num_detect_classes), d_tags.view(-1))
118
+ loss = loss_labels + loss_d
119
+
120
+ if not return_dict:
121
+ output = (logits, logits_d) + outputs[2:]
122
+ return ((loss,) + output) if loss is not None else output
123
+
124
+ return Seq2LabelsOutput(
125
+ loss=loss,
126
+ logits=logits,
127
+ detect_logits=logits_d,
128
+ hidden_states=outputs.hidden_states,
129
+ attentions=outputs.attentions,
130
+ max_error_probability=torch.ones(logits.size(0), device=logits.device),
131
+ )
utils.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ import re
4
+
5
+
6
+ VOCAB_DIR = Path(__file__).resolve().parent
7
+ PAD = "@@PADDING@@"
8
+ UNK = "@@UNKNOWN@@"
9
+ START_TOKEN = "$START"
10
+ SEQ_DELIMETERS = {"tokens": " ", "labels": "SEPL|||SEPR", "operations": "SEPL__SEPR"}
11
+
12
+
13
+ def get_verb_form_dicts():
14
+ path_to_dict = os.path.join(VOCAB_DIR, "verb-form-vocab.txt")
15
+ encode, decode = {}, {}
16
+ with open(path_to_dict, encoding="utf-8") as f:
17
+ for line in f:
18
+ words, tags = line.split(":")
19
+ word1, word2 = words.split("_")
20
+ tag1, tag2 = tags.split("_")
21
+ decode_key = f"{word1}_{tag1}_{tag2.strip()}"
22
+ if decode_key not in decode:
23
+ encode[words] = tags
24
+ decode[decode_key] = word2
25
+ return encode, decode
26
+
27
+
28
+ ENCODE_VERB_DICT, DECODE_VERB_DICT = get_verb_form_dicts()
29
+
30
+
31
+ def get_target_sent_by_edits(source_tokens, edits):
32
+ target_tokens = source_tokens[:]
33
+ shift_idx = 0
34
+ for edit in edits:
35
+ start, end, label, _ = edit
36
+ target_pos = start + shift_idx
37
+ if start < 0:
38
+ continue
39
+ elif len(target_tokens) > target_pos:
40
+ source_token = target_tokens[target_pos]
41
+ else:
42
+ source_token = ""
43
+ if label == "":
44
+ del target_tokens[target_pos]
45
+ shift_idx -= 1
46
+ elif start == end:
47
+ word = label.replace("$APPEND_", "")
48
+ # Avoid appending same token twice
49
+ if (target_pos < len(target_tokens) and target_tokens[target_pos] == word) or (
50
+ target_pos > 0 and target_tokens[target_pos - 1] == word
51
+ ):
52
+ continue
53
+ target_tokens[target_pos:target_pos] = [word]
54
+ shift_idx += 1
55
+ elif label.startswith("$TRANSFORM_"):
56
+ word = apply_reverse_transformation(source_token, label)
57
+ if word is None:
58
+ word = source_token
59
+ target_tokens[target_pos] = word
60
+ elif start == end - 1:
61
+ word = label.replace("$REPLACE_", "")
62
+ target_tokens[target_pos] = word
63
+ elif label.startswith("$MERGE_"):
64
+ target_tokens[target_pos + 1 : target_pos + 1] = [label]
65
+ shift_idx += 1
66
+
67
+ return replace_merge_transforms(target_tokens)
68
+
69
+
70
+ def replace_merge_transforms(tokens):
71
+ if all(not x.startswith("$MERGE_") for x in tokens):
72
+ return tokens
73
+ if tokens[0].startswith("$MERGE_"):
74
+ tokens = tokens[1:]
75
+ if tokens[-1].startswith("$MERGE_"):
76
+ tokens = tokens[:-1]
77
+
78
+ target_line = " ".join(tokens)
79
+ target_line = target_line.replace(" $MERGE_HYPHEN ", "-")
80
+ target_line = target_line.replace(" $MERGE_SPACE ", "")
81
+ target_line = re.sub(r'([\.\,\?\:]\s+)+', r'\1', target_line)
82
+ return target_line.split()
83
+
84
+
85
+ def convert_using_case(token, smart_action):
86
+ if not smart_action.startswith("$TRANSFORM_CASE_"):
87
+ return token
88
+ if smart_action.endswith("LOWER"):
89
+ return token.lower()
90
+ elif smart_action.endswith("UPPER"):
91
+ return token.upper()
92
+ elif smart_action.endswith("CAPITAL"):
93
+ return token.capitalize()
94
+ elif smart_action.endswith("CAPITAL_1"):
95
+ return token[0] + token[1:].capitalize()
96
+ elif smart_action.endswith("UPPER_-1"):
97
+ return token[:-1].upper() + token[-1]
98
+ else:
99
+ return token
100
+
101
+
102
+ def convert_using_verb(token, smart_action):
103
+ key_word = "$TRANSFORM_VERB_"
104
+ if not smart_action.startswith(key_word):
105
+ raise Exception(f"Unknown action type {smart_action}")
106
+ encoding_part = f"{token}_{smart_action[len(key_word):]}"
107
+ decoded_target_word = decode_verb_form(encoding_part)
108
+ return decoded_target_word
109
+
110
+
111
+ def convert_using_split(token, smart_action):
112
+ key_word = "$TRANSFORM_SPLIT"
113
+ if not smart_action.startswith(key_word):
114
+ raise Exception(f"Unknown action type {smart_action}")
115
+ target_words = token.split("-")
116
+ return " ".join(target_words)
117
+
118
+
119
+ def convert_using_plural(token, smart_action):
120
+ if smart_action.endswith("PLURAL"):
121
+ return token + "s"
122
+ elif smart_action.endswith("SINGULAR"):
123
+ return token[:-1]
124
+ else:
125
+ raise Exception(f"Unknown action type {smart_action}")
126
+
127
+
128
+ def apply_reverse_transformation(source_token, transform):
129
+ if transform.startswith("$TRANSFORM"):
130
+ # deal with equal
131
+ if transform == "$KEEP":
132
+ return source_token
133
+ # deal with case
134
+ if transform.startswith("$TRANSFORM_CASE"):
135
+ return convert_using_case(source_token, transform)
136
+ # deal with verb
137
+ if transform.startswith("$TRANSFORM_VERB"):
138
+ return convert_using_verb(source_token, transform)
139
+ # deal with split
140
+ if transform.startswith("$TRANSFORM_SPLIT"):
141
+ return convert_using_split(source_token, transform)
142
+ # deal with single/plural
143
+ if transform.startswith("$TRANSFORM_AGREEMENT"):
144
+ return convert_using_plural(source_token, transform)
145
+ # raise exception if not find correct type
146
+ raise Exception(f"Unknown action type {transform}")
147
+ else:
148
+ return source_token
149
+
150
+
151
+ # def read_parallel_lines(fn1, fn2):
152
+ # lines1 = read_lines(fn1, skip_strip=True)
153
+ # lines2 = read_lines(fn2, skip_strip=True)
154
+ # assert len(lines1) == len(lines2)
155
+ # out_lines1, out_lines2 = [], []
156
+ # for line1, line2 in zip(lines1, lines2):
157
+ # if not line1.strip() or not line2.strip():
158
+ # continue
159
+ # else:
160
+ # out_lines1.append(line1)
161
+ # out_lines2.append(line2)
162
+ # return out_lines1, out_lines2
163
+
164
+
165
+ def read_parallel_lines(fn1, fn2):
166
+ with open(fn1, encoding='utf-8') as f1, open(fn2, encoding='utf-8') as f2:
167
+ for line1, line2 in zip(f1, f2):
168
+ line1 = line1.strip()
169
+ line2 = line2.strip()
170
+
171
+ yield line1, line2
172
+
173
+
174
+ def read_lines(fn, skip_strip=False):
175
+ if not os.path.exists(fn):
176
+ return []
177
+ with open(fn, 'r', encoding='utf-8') as f:
178
+ lines = f.readlines()
179
+ return [s.strip() for s in lines if s.strip() or skip_strip]
180
+
181
+
182
+ def write_lines(fn, lines, mode='w'):
183
+ if mode == 'w' and os.path.exists(fn):
184
+ os.remove(fn)
185
+ with open(fn, encoding='utf-8', mode=mode) as f:
186
+ f.writelines(['%s\n' % s for s in lines])
187
+
188
+
189
+ def decode_verb_form(original):
190
+ return DECODE_VERB_DICT.get(original)
191
+
192
+
193
+ def encode_verb_form(original_word, corrected_word):
194
+ decoding_request = original_word + "_" + corrected_word
195
+ decoding_response = ENCODE_VERB_DICT.get(decoding_request, "").strip()
196
+ if original_word and decoding_response:
197
+ answer = decoding_response
198
+ else:
199
+ answer = None
200
+ return answer
201
+
202
+
203
+ def get_weights_name(transformer_name, lowercase):
204
+ if transformer_name == 'bert' and lowercase:
205
+ return 'bert-base-uncased'
206
+ if transformer_name == 'bert' and not lowercase:
207
+ return 'bert-base-cased'
208
+ if transformer_name == 'bert-large' and not lowercase:
209
+ return 'bert-large-cased'
210
+ if transformer_name == 'distilbert':
211
+ if not lowercase:
212
+ print('Warning! This model was trained only on uncased sentences.')
213
+ return 'distilbert-base-uncased'
214
+ if transformer_name == 'albert':
215
+ if not lowercase:
216
+ print('Warning! This model was trained only on uncased sentences.')
217
+ return 'albert-base-v1'
218
+ if lowercase:
219
+ print('Warning! This model was trained only on cased sentences.')
220
+ if transformer_name == 'roberta':
221
+ return 'roberta-base'
222
+ if transformer_name == 'roberta-large':
223
+ return 'roberta-large'
224
+ if transformer_name == 'gpt2':
225
+ return 'gpt2'
226
+ if transformer_name == 'transformerxl':
227
+ return 'transfo-xl-wt103'
228
+ if transformer_name == 'xlnet':
229
+ return 'xlnet-base-cased'
230
+ if transformer_name == 'xlnet-large':
231
+ return 'xlnet-large-cased'
232
+
233
+ return transformer_name
verb-form-vocab.txt ADDED
The diff for this file is too large to render. See raw diff
 
vibert-capu.int8.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:67278b23502bbd744538e2bd9a6748b61cce171cfba6c671c2e2a46f892166fa
3
+ size 115707731
vibert-capu.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:269a59c50977cef010292b1530a77df3073420ff6be409c1a55eeb77a8444e44
3
+ size 459449409
vocab.txt ADDED
The diff for this file is too large to render. See raw diff
 
vocabulary.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import codecs
2
+ from collections import defaultdict
3
+ import logging
4
+ import os
5
+ import re
6
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union, TYPE_CHECKING
7
+ from filelock import FileLock
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ DEFAULT_NON_PADDED_NAMESPACES = ("*tags", "*labels")
13
+ DEFAULT_PADDING_TOKEN = "@@PADDING@@"
14
+ DEFAULT_OOV_TOKEN = "@@UNKNOWN@@"
15
+ NAMESPACE_PADDING_FILE = "non_padded_namespaces.txt"
16
+ _NEW_LINE_REGEX = re.compile(r"\n|\r\n")
17
+
18
+
19
+ def namespace_match(pattern: str, namespace: str):
20
+ """
21
+ Matches a namespace pattern against a namespace string. For example, `*tags` matches
22
+ `passage_tags` and `question_tags` and `tokens` matches `tokens` but not
23
+ `stemmed_tokens`.
24
+ """
25
+ if pattern[0] == "*" and namespace.endswith(pattern[1:]):
26
+ return True
27
+ elif pattern == namespace:
28
+ return True
29
+ return False
30
+
31
+
32
+ class _NamespaceDependentDefaultDict(defaultdict):
33
+ """
34
+ This is a [defaultdict]
35
+ (https://docs.python.org/2/library/collections.html#collections.defaultdict) where the
36
+ default value is dependent on the key that is passed.
37
+ We use "namespaces" in the :class:`Vocabulary` object to keep track of several different
38
+ mappings from strings to integers, so that we have a consistent API for mapping words, tags,
39
+ labels, characters, or whatever else you want, into integers. The issue is that some of those
40
+ namespaces (words and characters) should have integers reserved for padding and
41
+ out-of-vocabulary tokens, while others (labels and tags) shouldn't. This class allows you to
42
+ specify filters on the namespace (the key used in the `defaultdict`), and use different
43
+ default values depending on whether the namespace passes the filter.
44
+ To do filtering, we take a set of `non_padded_namespaces`. This is a set of strings
45
+ that are either matched exactly against the keys, or treated as suffixes, if the
46
+ string starts with `*`. In other words, if `*tags` is in `non_padded_namespaces` then
47
+ `passage_tags`, `question_tags`, etc. (anything that ends with `tags`) will have the
48
+ `non_padded` default value.
49
+ # Parameters
50
+ non_padded_namespaces : `Iterable[str]`
51
+ A set / list / tuple of strings describing which namespaces are not padded. If a namespace
52
+ (key) is missing from this dictionary, we will use :func:`namespace_match` to see whether
53
+ the namespace should be padded. If the given namespace matches any of the strings in this
54
+ list, we will use `non_padded_function` to initialize the value for that namespace, and
55
+ we will use `padded_function` otherwise.
56
+ padded_function : `Callable[[], Any]`
57
+ A zero-argument function to call to initialize a value for a namespace that `should` be
58
+ padded.
59
+ non_padded_function : `Callable[[], Any]`
60
+ A zero-argument function to call to initialize a value for a namespace that should `not` be
61
+ padded.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ non_padded_namespaces: Iterable[str],
67
+ padded_function: Callable[[], Any],
68
+ non_padded_function: Callable[[], Any],
69
+ ) -> None:
70
+ self._non_padded_namespaces = set(non_padded_namespaces)
71
+ self._padded_function = padded_function
72
+ self._non_padded_function = non_padded_function
73
+ super().__init__()
74
+
75
+ def add_non_padded_namespaces(self, non_padded_namespaces: Set[str]):
76
+ # add non_padded_namespaces which weren't already present
77
+ self._non_padded_namespaces.update(non_padded_namespaces)
78
+
79
+
80
+ class _TokenToIndexDefaultDict(_NamespaceDependentDefaultDict):
81
+ def __init__(self, non_padded_namespaces: Set[str], padding_token: str, oov_token: str) -> None:
82
+ super().__init__(non_padded_namespaces, lambda: {padding_token: 0, oov_token: 1}, lambda: {})
83
+
84
+
85
+ class _IndexToTokenDefaultDict(_NamespaceDependentDefaultDict):
86
+ def __init__(self, non_padded_namespaces: Set[str], padding_token: str, oov_token: str) -> None:
87
+ super().__init__(non_padded_namespaces, lambda: {0: padding_token, 1: oov_token}, lambda: {})
88
+
89
+
90
+ class Vocabulary:
91
+ def __init__(
92
+ self,
93
+ counter: Dict[str, Dict[str, int]] = None,
94
+ min_count: Dict[str, int] = None,
95
+ max_vocab_size: Union[int, Dict[str, int]] = None,
96
+ non_padded_namespaces: Iterable[str] = DEFAULT_NON_PADDED_NAMESPACES,
97
+ pretrained_files: Optional[Dict[str, str]] = None,
98
+ only_include_pretrained_words: bool = False,
99
+ tokens_to_add: Dict[str, List[str]] = None,
100
+ min_pretrained_embeddings: Dict[str, int] = None,
101
+ padding_token: Optional[str] = DEFAULT_PADDING_TOKEN,
102
+ oov_token: Optional[str] = DEFAULT_OOV_TOKEN,
103
+ ) -> None:
104
+ self._padding_token = padding_token if padding_token is not None else DEFAULT_PADDING_TOKEN
105
+ self._oov_token = oov_token if oov_token is not None else DEFAULT_OOV_TOKEN
106
+
107
+ self._non_padded_namespaces = set(non_padded_namespaces)
108
+
109
+ self._token_to_index = _TokenToIndexDefaultDict(
110
+ self._non_padded_namespaces, self._padding_token, self._oov_token
111
+ )
112
+ self._index_to_token = _IndexToTokenDefaultDict(
113
+ self._non_padded_namespaces, self._padding_token, self._oov_token
114
+ )
115
+
116
+ @classmethod
117
+ def from_files(
118
+ cls,
119
+ directory: Union[str, os.PathLike],
120
+ padding_token: Optional[str] = DEFAULT_PADDING_TOKEN,
121
+ oov_token: Optional[str] = DEFAULT_OOV_TOKEN,
122
+ ) -> "Vocabulary":
123
+ """
124
+ Loads a `Vocabulary` that was serialized either using `save_to_files` or inside
125
+ a model archive file.
126
+ # Parameters
127
+ directory : `str`
128
+ The directory or archive file containing the serialized vocabulary.
129
+ """
130
+ logger.info("Loading token dictionary from %s.", directory)
131
+ padding_token = padding_token if padding_token is not None else DEFAULT_PADDING_TOKEN
132
+ oov_token = oov_token if oov_token is not None else DEFAULT_OOV_TOKEN
133
+
134
+ if not os.path.isdir(directory):
135
+ raise ValueError(f"{directory} not exist")
136
+
137
+ # We use a lock file to avoid race conditions where multiple processes
138
+ # might be reading/writing from/to the same vocab files at once.
139
+ with FileLock(os.path.join(directory, ".lock")):
140
+ with codecs.open(os.path.join(directory, NAMESPACE_PADDING_FILE), "r", "utf-8") as namespace_file:
141
+ non_padded_namespaces = [namespace_str.strip() for namespace_str in namespace_file]
142
+
143
+ vocab = cls(
144
+ non_padded_namespaces=non_padded_namespaces,
145
+ padding_token=padding_token,
146
+ oov_token=oov_token,
147
+ )
148
+
149
+ # Check every file in the directory.
150
+ for namespace_filename in os.listdir(directory):
151
+ if namespace_filename == NAMESPACE_PADDING_FILE:
152
+ continue
153
+ if namespace_filename.startswith("."):
154
+ continue
155
+ namespace = namespace_filename.replace(".txt", "")
156
+ if any(namespace_match(pattern, namespace) for pattern in non_padded_namespaces):
157
+ is_padded = False
158
+ else:
159
+ is_padded = True
160
+ filename = os.path.join(directory, namespace_filename)
161
+ vocab.set_from_file(filename, is_padded, namespace=namespace, oov_token=oov_token)
162
+
163
+ return vocab
164
+
165
+ @classmethod
166
+ def empty(cls) -> "Vocabulary":
167
+ """
168
+ This method returns a bare vocabulary instantiated with `cls()` (so, `Vocabulary()` if you
169
+ haven't made a subclass of this object). The only reason to call `Vocabulary.empty()`
170
+ instead of `Vocabulary()` is if you are instantiating this object from a config file. We
171
+ register this constructor with the key "empty", so if you know that you don't need to
172
+ compute a vocabulary (either because you're loading a pre-trained model from an archive
173
+ file, you're using a pre-trained transformer that has its own vocabulary, or something
174
+ else), you can use this to avoid having the default vocabulary construction code iterate
175
+ through the data.
176
+ """
177
+ return cls()
178
+
179
+ def set_from_file(
180
+ self,
181
+ filename: str,
182
+ is_padded: bool = True,
183
+ oov_token: str = DEFAULT_OOV_TOKEN,
184
+ namespace: str = "tokens",
185
+ ):
186
+ """
187
+ If you already have a vocabulary file for a trained model somewhere, and you really want to
188
+ use that vocabulary file instead of just setting the vocabulary from a dataset, for
189
+ whatever reason, you can do that with this method. You must specify the namespace to use,
190
+ and we assume that you want to use padding and OOV tokens for this.
191
+ # Parameters
192
+ filename : `str`
193
+ The file containing the vocabulary to load. It should be formatted as one token per
194
+ line, with nothing else in the line. The index we assign to the token is the line
195
+ number in the file (1-indexed if `is_padded`, 0-indexed otherwise). Note that this
196
+ file should contain the OOV token string!
197
+ is_padded : `bool`, optional (default=`True`)
198
+ Is this vocabulary padded? For token / word / character vocabularies, this should be
199
+ `True`; while for tag or label vocabularies, this should typically be `False`. If
200
+ `True`, we add a padding token with index 0, and we enforce that the `oov_token` is
201
+ present in the file.
202
+ oov_token : `str`, optional (default=`DEFAULT_OOV_TOKEN`)
203
+ What token does this vocabulary use to represent out-of-vocabulary characters? This
204
+ must show up as a line in the vocabulary file. When we find it, we replace
205
+ `oov_token` with `self._oov_token`, because we only use one OOV token across
206
+ namespaces.
207
+ namespace : `str`, optional (default=`"tokens"`)
208
+ What namespace should we overwrite with this vocab file?
209
+ """
210
+ if is_padded:
211
+ self._token_to_index[namespace] = {self._padding_token: 0}
212
+ self._index_to_token[namespace] = {0: self._padding_token}
213
+ else:
214
+ self._token_to_index[namespace] = {}
215
+ self._index_to_token[namespace] = {}
216
+ with codecs.open(filename, "r", "utf-8") as input_file:
217
+ lines = _NEW_LINE_REGEX.split(input_file.read())
218
+ # Be flexible about having final newline or not
219
+ if lines and lines[-1] == "":
220
+ lines = lines[:-1]
221
+ for i, line in enumerate(lines):
222
+ index = i + 1 if is_padded else i
223
+ token = line.replace("@@NEWLINE@@", "\n")
224
+ if token == oov_token:
225
+ token = self._oov_token
226
+ self._token_to_index[namespace][token] = index
227
+ self._index_to_token[namespace][index] = token
228
+ if is_padded:
229
+ assert self._oov_token in self._token_to_index[namespace], "OOV token not found!"
230
+
231
+ def add_token_to_namespace(self, token: str, namespace: str = "tokens") -> int:
232
+ """
233
+ Adds `token` to the index, if it is not already present. Either way, we return the index of
234
+ the token.
235
+ """
236
+ if not isinstance(token, str):
237
+ raise ValueError(
238
+ "Vocabulary tokens must be strings, or saving and loading will break."
239
+ " Got %s (with type %s)" % (repr(token), type(token))
240
+ )
241
+ if token not in self._token_to_index[namespace]:
242
+ index = len(self._token_to_index[namespace])
243
+ self._token_to_index[namespace][token] = index
244
+ self._index_to_token[namespace][index] = token
245
+ return index
246
+ else:
247
+ return self._token_to_index[namespace][token]
248
+
249
+ def add_tokens_to_namespace(self, tokens: List[str], namespace: str = "tokens") -> List[int]:
250
+ """
251
+ Adds `tokens` to the index, if they are not already present. Either way, we return the
252
+ indices of the tokens in the order that they were given.
253
+ """
254
+ return [self.add_token_to_namespace(token, namespace) for token in tokens]
255
+
256
+ def get_token_index(self, token: str, namespace: str = "tokens") -> int:
257
+ try:
258
+ return self._token_to_index[namespace][token]
259
+ except KeyError:
260
+ try:
261
+ return self._token_to_index[namespace][self._oov_token]
262
+ except KeyError:
263
+ logger.error("Namespace: %s", namespace)
264
+ logger.error("Token: %s", token)
265
+ raise KeyError(
266
+ f"'{token}' not found in vocab namespace '{namespace}', and namespace "
267
+ f"does not contain the default OOV token ('{self._oov_token}')"
268
+ )
269
+
270
+ def get_token_from_index(self, index: int, namespace: str = "tokens") -> str:
271
+ return self._index_to_token[namespace][index]
272
+
273
+ def get_vocab_size(self, namespace: str = "tokens") -> int:
274
+ return len(self._token_to_index[namespace])
275
+
276
+ def get_namespaces(self) -> Set[str]:
277
+ return set(self._index_to_token.keys())
vocabulary/d_tags.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ CORRECT
2
+ INCORRECT
3
+ @@UNKNOWN@@
4
+ @@PADDING@@
vocabulary/labels.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ $KEEP
2
+ $TRANSFORM_CASE_CAPITAL
3
+ $APPEND_,
4
+ $APPEND_.
5
+ $TRANSFORM_VERB_VB_VBN
6
+ $TRANSFORM_CASE_UPPER
7
+ $APPEND_:
8
+ $APPEND_?
9
+ $TRANSFORM_VERB_VB_VBC
10
+ $TRANSFORM_CASE_LOWER
11
+ $TRANSFORM_CASE_CAPITAL_1
12
+ $TRANSFORM_CASE_UPPER_-1
13
+ $MERGE_SPACE
14
+ @@UNKNOWN@@
15
+ @@PADDING@@
vocabulary/non_padded_namespaces.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *tags
2
+ *labels