|
7 | 7 | from torch.utils.data.sampler import Sampler |
8 | 8 |
|
9 | 9 | from stanza.models.common.bert_embedding import filter_data, needs_length_filter |
10 | | -from stanza.models.common.data import map_to_ids, get_long_tensor, get_float_tensor, sort_all, get_augment_ratio |
| 10 | +from stanza.models.common.data import map_to_ids, get_long_tensor, get_float_tensor, sort_all, get_augment_ratio, INITIAL_INVERTED_PUNCT_MARKS, starts_with_initial_mark |
11 | 11 | from stanza.models.common.utils import DEFAULT_WORD_CUTOFF, simplify_punct |
12 | 12 | from stanza.models.common.vocab import PAD_ID, VOCAB_PREFIX, ROOT_ID, CompositeVocab, CharVocab |
13 | 13 | from stanza.models.pos.vocab import WordVocab, XPOSVocab, FeatureVocab, MultiVocab |
@@ -104,6 +104,75 @@ def record_can_augment_nopunct(record, punct_id): |
104 | 104 | return True |
105 | 105 |
|
106 | 106 |
|
| 107 | +def record_starts_with_mark(record, marks=INITIAL_INVERTED_PUNCT_MARKS): |
| 108 | + """ |
| 109 | + True if this preprocessed sentence's first real word is exactly one |
| 110 | + of the given marks (by default the Spanish/Catalan inverted question |
| 111 | + and exclamation marks), and no mark from the set appears anywhere |
| 112 | + else in the sentence. |
| 113 | +
|
| 114 | + Thin wrapper around stanza.models.common.data.starts_with_initial_mark |
| 115 | + -- record[9] is the plain-string text field (never vocab-mapped), |
| 116 | + which is exactly the "list of word strings" that function expects. |
| 117 | + The actual mark-matching logic (including the no-other-mark |
| 118 | + restriction, mirroring augment_initial_punct in |
| 119 | + prepare_tokenizer_treebank.py) lives there in one place, shared with |
| 120 | + the POS tagger's equivalent eligibility check; only this record[9] |
| 121 | + extraction is specific to the parser's own record shape. |
| 122 | + """ |
| 123 | + return starts_with_initial_mark(record[9], marks) |
| 124 | + |
| 125 | + |
| 126 | +def record_can_drop_initial_mark(record, marks=INITIAL_INVERTED_PUNCT_MARKS): |
| 127 | + """ |
| 128 | + True if the sentence starts with one of the given marks (see |
| 129 | + record_starts_with_mark) and no other word's head depends directly |
| 130 | + on that first token -- removing it would otherwise leave a word's |
| 131 | + head pointing at a position that no longer exists. |
| 132 | +
|
| 133 | + record[7] is the head field (no ROOT, one 1-indexed position per |
| 134 | + real word); the first real word's own 1-indexed position is always 1. |
| 135 | + """ |
| 136 | + if not record_starts_with_mark(record, marks): |
| 137 | + return False |
| 138 | + head = record[7] |
| 139 | + if any(h == 1 for h in head[1:]): |
| 140 | + return False |
| 141 | + return True |
| 142 | + |
| 143 | + |
| 144 | +def drop_initial_mark_from_record(record): |
| 145 | + """ |
| 146 | + Removes the first real word from a preprocessed sentence, renumbering |
| 147 | + every remaining word's head position down by one to account for it. |
| 148 | +
|
| 149 | + Unlike dropping the last word (record_can_augment_nopunct's case), |
| 150 | + dropping the FIRST word shifts every later word's 1-indexed position |
| 151 | + back by one, so every head value greater than 0 must be decremented; |
| 152 | + a head of 0 (root) is left untouched. record_can_drop_initial_mark |
| 153 | + already guarantees no remaining word's head is exactly 1 (i.e. |
| 154 | + nothing depends on the word being removed), so every nonzero head |
| 155 | + among the remaining words is guaranteed to be >= 2 before the shift. |
| 156 | + """ |
| 157 | + word, char, upos, xpos, feats, pretrain, lemma, head, deprel, text = record |
| 158 | + # ROOT-prepended fields: keep ROOT (index 0), drop the removed word |
| 159 | + # (index 1), keep everything after it unchanged |
| 160 | + new_word = [word[0]] + word[2:] |
| 161 | + new_char = [char[0]] + char[2:] |
| 162 | + new_upos = [upos[0]] + upos[2:] |
| 163 | + new_xpos = [xpos[0]] + xpos[2:] |
| 164 | + new_feats = [feats[0]] + feats[2:] |
| 165 | + new_pretrain = [pretrain[0]] + pretrain[2:] |
| 166 | + new_lemma = [lemma[0]] + lemma[2:] |
| 167 | + # non-ROOT-prepended fields: drop the removed word's own entry, and |
| 168 | + # shift every remaining head position down by one |
| 169 | + new_head = [h - 1 if h > 0 else h for h in head[1:]] |
| 170 | + new_deprel = deprel[1:] |
| 171 | + new_text = text[1:] |
| 172 | + return [new_word, new_char, new_upos, new_xpos, new_feats, new_pretrain, |
| 173 | + new_lemma, new_head, new_deprel, new_text] |
| 174 | + |
| 175 | + |
107 | 176 | class Dataset: |
108 | 177 | """ |
109 | 178 | Sentence-level dataset for the dependency parser: owns vocab |
@@ -187,6 +256,23 @@ def __init__(self, doc, args, pretrain, vocab=None, evaluation=False, bert_token |
187 | 256 | else: |
188 | 257 | self.augment_nopunct_ratio = augment_nopunct_arg |
189 | 258 |
|
| 259 | + # dynamic leading-inverted-punct drop: some UD treebanks (Spanish, |
| 260 | + # Catalan) have every training sentence begin with an inverted |
| 261 | + # question or exclamation mark (¿/¡), which the model never learns |
| 262 | + # to do without. Mirrors augment_initial_punct in |
| 263 | + # prepare_tokenizer_treebank.py, applied per sentence in |
| 264 | + # __getitem__ instead of by duplicating sentences at dataset- |
| 265 | + # preparation time. Unlike augment_nopunct, this uses a flat |
| 266 | + # default ratio (not an auto-detected one), matching the ratio |
| 267 | + # parameter augment_initial_punct itself takes. |
| 268 | + drop_initial_punct_arg = args.get('drop_initial_punct_prob', 0.20) |
| 269 | + if self.eval or not drop_initial_punct_arg or drop_initial_punct_arg <= 0: |
| 270 | + self.drop_initial_punct_eligible = False |
| 271 | + self.drop_initial_punct_ratio = 0.0 |
| 272 | + else: |
| 273 | + self.drop_initial_punct_eligible = any(record_starts_with_mark(record) for record in self.data) |
| 274 | + self.drop_initial_punct_ratio = drop_initial_punct_arg if self.drop_initial_punct_eligible else 0.0 |
| 275 | + |
190 | 276 | def init_vocab(self, data): |
191 | 277 | assert self.eval == False # for eval vocab must exist |
192 | 278 | cutoff = self.args['word_cutoff'] if self.args.get('word_cutoff') is not None else DEFAULT_WORD_CUTOFF |
@@ -250,6 +336,11 @@ def __getitem__(self, key): |
250 | 336 | # lemma) and non-ROOT-prepended fields (head/deprel/text) |
251 | 337 | # all simply lose their last entry |
252 | 338 | record = [field[:-1] for field in record] |
| 339 | + if self.drop_initial_punct_ratio > 0 and record_can_drop_initial_mark(record): |
| 340 | + if random.random() < self.drop_initial_punct_ratio: |
| 341 | + # drop the leading ¿/¡ and renumber every remaining word's |
| 342 | + # head position down by one -- see drop_initial_mark_from_record |
| 343 | + record = drop_initial_mark_from_record(record) |
253 | 344 | return record |
254 | 345 |
|
255 | 346 | def __iter__(self): |
|
0 commit comments