Ericu950 commited on
Commit
da17d2f
·
verified ·
1 Parent(s): 5cc4a70

Add ud=True flag to decode(): converts native Perseus/AGDT output to Universal Dependencies (HamleDT-style COORD/AuxP/Pnom restructuring + XPOS->UPOS+FEATS mapping)

Browse files
Files changed (1) hide show
  1. processing_char_bert_joint.py +236 -4
processing_char_bert_joint.py CHANGED
@@ -4,7 +4,11 @@ Turns one or more already word-tokenized sentences into the model's input tensor
4
  four character planes as the base CharBertProcessor, plus a `word_id` array pooling characters
5
  into words, a `cap` plane -- an actual model input for this fine-tuned checkpoint, unlike the
6
  base model where capitalization is output-only -- and a `seg_id` plane), and decodes the
7
- model's raw logits back into UD-style XPOS / lemma / UPOS / dependency-arc predictions.
 
 
 
 
8
 
9
  Interface: call the processor with `List[str]` (one already-tokenized sentence, e.g.
10
  `["ὁ", "ἄνθρωπος", "τρέχει", "."]`) or `List[List[str]]` (a batch of such sentences) --
@@ -57,6 +61,225 @@ def _pack_dia(acc, br, iota, diaer):
57
  return ((acc * 3 + br) * 2 + iota) * 2 + diaer
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  # ---- edit-script machinery (tagger/edits.py, ported verbatim on strings) ---------------------
61
 
62
  GRAVE, ACUTE = "̀", "́"
@@ -355,10 +578,19 @@ class CharBertJointProcessor:
355
 
356
  # ---------------------------------------------------------------- full decode
357
 
358
- def decode(self, model_out, batch, use_lexicon: bool = True) -> list[list[dict]]:
359
  """model_out: a CharBertJointOutput (or the equivalent return_dict=False tuple).
360
  batch: the dict returned by __call__ (needs `_forms`).
361
- -> per sentence, a list of {form, xpos, lemma, upos, head, deprel} dicts, word order."""
 
 
 
 
 
 
 
 
 
362
  word_mask = model_out.word_mask if hasattr(model_out, "word_mask") else model_out[-1]
363
  xpos_logits = model_out.xpos_logits if hasattr(model_out, "xpos_logits") else model_out[0]
364
  script_logits = model_out.script_logits if hasattr(model_out, "script_logits") else model_out[1]
@@ -390,5 +622,5 @@ class CharBertJointProcessor:
390
  if labels_out is not None and self.deprel_vocab else None)
391
  sent_out.append(dict(form=form, xpos=xpos, lemma=lemma, upos=upos,
392
  head=head, deprel=deprel))
393
- results.append(sent_out)
394
  return results
 
4
  four character planes as the base CharBertProcessor, plus a `word_id` array pooling characters
5
  into words, a `cap` plane -- an actual model input for this fine-tuned checkpoint, unlike the
6
  base model where capitalization is output-only -- and a `seg_id` plane), and decodes the
7
+ model's raw logits back into predictions. The model's NATIVE tag/relation inventory is the
8
+ Perseus/AGDT scheme (9-position morphological tags; Prague-style dependency relations like
9
+ SBJ/OBJ/ATR/COORD/AuxP -- the scheme its OGA training treebank uses), not Universal
10
+ Dependencies. Pass `ud=True` to `decode()` for Universal Dependencies-style output instead
11
+ (UPOS/FEATS/deprel) -- see `to_ud()` below for the conversion and its scope/limitations.
12
 
13
  Interface: call the processor with `List[str]` (one already-tokenized sentence, e.g.
14
  `["ὁ", "ἄνθρωπος", "τρέχει", "."]`) or `List[List[str]]` (a batch of such sentences) --
 
61
  return ((acc * 3 + br) * 2 + iota) * 2 + diaer
62
 
63
 
64
+ # ==== Universal Dependencies conversion ========================================================
65
+ #
66
+ # The model's native output is the Perseus/AGDT annotation scheme its OGA training treebank
67
+ # uses: a 9-position morphological tag (pos.person.number.tense.mood.voice.gender.case.degree,
68
+ # one letter per position, "-" for n/a) and Prague-style dependency relations (SBJ, OBJ, ATR,
69
+ # ADV, PNOM, PRED, APOS, COORD, Aux*, ATV/AtvV, ExD, MWE, OCOMP) -- NOT Universal Dependencies.
70
+ #
71
+ # This is a solved, documented conversion problem, not something designed from scratch here:
72
+ # both UD_Ancient_Greek-Perseus and UD_Ancient_Greek-PROIEL are themselves automatic
73
+ # conversions of exactly this annotation style, via Zeman & Ramasamy's HamleDT pipeline
74
+ # (Zeman et al. 2014, "HamleDT: Harmonized Multi-Language Dependency Treebank", LRE) --
75
+ # specifically Treex::Block::HamleDT::{GRC::Harmonize,HarmonizePerseus,Udep} and
76
+ # Treex::Tool::PhraseBuilder::PragueToUD (github.com/ufal/treex), plus the morphology decoder
77
+ # Lingua::Interset::Tagset::GRC::Conll (github.com/dan-zeman/interset). The tables and
78
+ # restructuring rules below port that design to our exact tag/relation inventory.
79
+ #
80
+ # Two relations require real TREE restructuring, not just a label rename, because UD attaches
81
+ # function words/conjuncts differently than Prague style:
82
+ # - COORD: Prague style makes the coordinating conjunction the head, conjuncts its children.
83
+ # UD makes the FIRST conjunct the head, the conjunction becomes a `cc` dependent of it, and
84
+ # other conjuncts become `conj` dependents.
85
+ # - AuxP: Prague style makes the preposition the head of its complement. UD reverses this:
86
+ # the nominal complement becomes the head, the preposition becomes a `case` dependent of it.
87
+ # - Pnom (predicate nominal / copula): Prague style makes the copula verb the head with the
88
+ # nominal attached as Pnom. UD makes the NOMINAL the head, the copula a `cop` dependent,
89
+ # and reattaches the subject to the nominal instead of the verb.
90
+ #
91
+ # What relation does the PROMOTED node (first conjunct / nominal complement) get in UD, given
92
+ # it used to be a plain dependent of COORD/AuxP? In full AGDT/HamleDT, the coordinator's or
93
+ # preposition's own outbound relation carries the phrase's true external function via a
94
+ # _CO/_AP label suffix (e.g. "Sb_Co"), which HamleDT strips off and hands to the promoted node.
95
+ # Our 24-label inventory has no such suffix mechanism -- verified directly against real training
96
+ # examples in oga_sota.conllu (e.g. "ἀπὸ" and "ἕως" each attach to their external governor with
97
+ # deprel=AuxP verbatim, and "καὶ" attaches with deprel=COORD verbatim, regardless of whether the
98
+ # phrase functions as a locative adjunct, a coordinated subject, or anything else). So the label
99
+ # itself never carries a recoverable function here, and the promoted node gets UD's generic
100
+ # fallback instead of a falsely precise guess: "obl" for AuxP nominals, "dep" for COORD first
101
+ # conjuncts (see Stage 1/2 below).
102
+ #
103
+ # SCOPE: this ports the core, well-documented HamleDT/UD restructuring rules and a straight
104
+ # per-label relabeling for everything else. It does NOT replicate every Ancient-Greek-specific
105
+ # refinement in the full pipeline (e.g. GRC::Harmonize's regex-based negation-particle
106
+ # splitting, or the is_member/shared-modifier _CO/_AP distinction discussed above) -- treat this
107
+ # as a faithful, reasonably-scoped port, not a byte-exact reproduction of the official
108
+ # UD_Ancient_Greek-Perseus release.
109
+
110
+ # position 1 (POS) -> UD UPOS. 'c' defaults to CCONJ, refined to SCONJ if its converted deprel
111
+ # is "mark" (i.e. it was AuxC); 'v' stays VERB regardless of mood (participles etc. get
112
+ # VerbForm=Part as a feature, not a UPOS change, matching standard UD_Ancient_Greek practice).
113
+ _POS_TO_UPOS = {
114
+ "a": "ADJ", "c": "CCONJ", "d": "ADV", "g": "PART", "i": "INTJ", "l": "DET",
115
+ "m": "NUM", "n": "NOUN", "p": "PRON", "r": "ADP", "u": "PUNCT", "v": "VERB", "x": "X",
116
+ }
117
+ _NUMBER = {"d": "Dual", "p": "Plur", "s": "Sing"}
118
+ _TENSE_ASPECT = { # (Tense, Aspect-or-None); aorist/perfect/future-perfect analysis per
119
+ "a": ("Past", "Perf"), # aorist = perfective past
120
+ "f": ("Fut", None),
121
+ "i": ("Past", "Imp"), # imperfect
122
+ "l": ("Pqp", None), # pluperfect
123
+ "p": ("Pres", None),
124
+ "r": ("Pres", "Perf"), # perfect = present relevance of a completed action
125
+ "t": ("Fut", "Perf"), # future perfect
126
+ }
127
+ _MOOD_VERBFORM = { # (Mood-or-None, VerbForm)
128
+ "i": ("Ind", "Fin"), "s": ("Sub", "Fin"), "o": ("Opt", "Fin"), "m": ("Imp", "Fin"),
129
+ "n": (None, "Inf"), "p": (None, "Part"), "g": (None, "Gdv"),
130
+ }
131
+ _VOICE = {"a": "Act", "m": "Mid", "p": "Pass", "e": "Mid"} # e = mediopassive, approximated as Mid
132
+ _GENDER = {"c": "Com", "f": "Fem", "m": "Masc", "n": "Neut"}
133
+ _CASE = {"a": "Acc", "d": "Dat", "g": "Gen", "n": "Nom", "v": "Voc"}
134
+ _DEGREE = {"c": "Cmp", "s": "Sup"}
135
+
136
+
137
+ def xpos_to_upos_feats(xpos: str) -> tuple[str, dict]:
138
+ """9-position Perseus/AGDT tag (e.g. "v3spia---") -> (UD UPOS, UD FEATS dict).
139
+ "-" in any position means not applicable/unspecified and is simply omitted from FEATS."""
140
+ pos = xpos[0] if xpos else "x"
141
+ upos = _POS_TO_UPOS.get(pos, "X")
142
+ feats = {}
143
+ rest = xpos[1:].ljust(8, "-")
144
+ person, number, tense, mood, voice, gender, case, degree = rest[:8]
145
+ if person in "123":
146
+ feats["Person"] = person
147
+ if number in _NUMBER:
148
+ feats["Number"] = _NUMBER[number]
149
+ if tense in _TENSE_ASPECT:
150
+ t, a = _TENSE_ASPECT[tense]
151
+ feats["Tense"] = t
152
+ if a:
153
+ feats["Aspect"] = a
154
+ if mood in _MOOD_VERBFORM:
155
+ m, vf = _MOOD_VERBFORM[mood]
156
+ if m:
157
+ feats["Mood"] = m
158
+ feats["VerbForm"] = vf
159
+ if voice in _VOICE:
160
+ feats["Voice"] = _VOICE[voice]
161
+ if gender in _GENDER:
162
+ feats["Gender"] = _GENDER[gender]
163
+ if case in _CASE:
164
+ feats["Case"] = _CASE[case]
165
+ if degree in _DEGREE:
166
+ feats["Degree"] = _DEGREE[degree]
167
+ return upos, feats
168
+
169
+
170
+ # Straight per-label relabeling for everything that ISN'T COORD/AuxP/Pnom (those need the tree
171
+ # restructuring in to_ud() below). Homoglyph artifacts in the training treebank (Greek "Ζ"/"Κ"
172
+ # instead of Latin "Z"/"K" in a couple of AuxZ/AuxK instances) are normalized first.
173
+ _HOMOGLYPH_FIX = {"AuxΖ": "AuxZ", "AuxΚ": "AuxK"}
174
+ _SIMPLE_DEPREL = {
175
+ "SBJ": "nsubj", "OBJ": "obj", "OCOMP": "obj", "APOS": "appos",
176
+ "AuxC": "mark", "AuxV": "aux", "AuxG": "punct", "AuxK": "punct", "AuxX": "punct",
177
+ "AuxR": "expl", "AuxY": "cc", "AuxZ": "advmod", "AuxΖ": "advmod", "AuxΚ": "punct",
178
+ "ATV": "xcomp", "AtvV": "xcomp", "ExD": "orphan", "MWE": "fixed",
179
+ "PRED": "root",
180
+ }
181
+
182
+
183
+ def _normalize_deprel(label: str | None) -> str | None:
184
+ return _HOMOGLYPH_FIX.get(label, label)
185
+
186
+
187
+ def to_ud(sent: list[dict]) -> list[dict]:
188
+ """Convert one sentence's native decode() output (list of {form, xpos, lemma, upos, head,
189
+ deprel} dicts, 1-indexed word order, head=0 meaning root) into UD-style output: each dict
190
+ becomes {form, lemma, upos (UD), xpos (native tag, kept as-is per UD convention that XPOS
191
+ is language-specific and optional), feats (dict), head, deprel (UD)}.
192
+
193
+ See the module-level comment above for the restructuring rules and scope/limitations.
194
+ """
195
+ n = len(sent)
196
+ if n == 0:
197
+ return []
198
+ heads = [0] * (n + 1)
199
+ deprels = [None] * (n + 1)
200
+ native_upos = [None] * (n + 1)
201
+ for i, w in enumerate(sent, start=1):
202
+ heads[i] = w["head"] if w["head"] is not None else 0
203
+ deprels[i] = _normalize_deprel(w["deprel"])
204
+ native_upos[i] = w["upos"]
205
+
206
+ def children_of():
207
+ c = {i: [] for i in range(0, n + 1)}
208
+ for i in range(1, n + 1):
209
+ c.setdefault(heads[i], []).append(i)
210
+ return c
211
+
212
+ # Stage 1: COORD -> first-conjunct-head restructuring.
213
+ for c in [i for i in range(1, n + 1) if deprels[i] == "COORD"]:
214
+ kids = sorted(children_of().get(c, []))
215
+ if not kids:
216
+ continue
217
+ first, rest = kids[0], kids[1:]
218
+ heads[first] = heads[c]
219
+ deprels[first] = "dep" # COORD's own outbound label carries no recoverable function; see above
220
+ heads[c] = first
221
+ deprels[c] = "cc"
222
+ for k in rest:
223
+ heads[k] = first
224
+ if deprels[k] not in ("AuxG", "AuxK", "AuxX", "AuxΖ", "AuxΚ"): # not a punctuation child
225
+ deprels[k] = "conj"
226
+ # punctuation children just reattach to `first`, keeping their own (later-relabeled) deprel
227
+
228
+ # Stage 2: AuxP -> case restructuring (nominal complement becomes the head).
229
+ for p in [i for i in range(1, n + 1) if deprels[i] == "AuxP"]:
230
+ kids = sorted(children_of().get(p, []))
231
+ if not kids:
232
+ continue
233
+ nominal, others = kids[0], kids[1:]
234
+ heads[nominal] = heads[p]
235
+ deprels[nominal] = "obl" # AuxP's own outbound label carries no recoverable function; see above
236
+ heads[p] = nominal
237
+ deprels[p] = "case"
238
+ for o in others:
239
+ heads[o] = nominal
240
+
241
+ # Stage 3: Pnom (predicate nominal) -> copula reversal. The nominal becomes the head in
242
+ # place of the copula verb; the verb becomes `cop`; anything else attached to the verb
243
+ # (chiefly the subject) is reattached to the nominal.
244
+ for nom in [i for i in range(1, n + 1) if deprels[i] == "PNOM"]:
245
+ verb = heads[nom]
246
+ if verb == 0:
247
+ continue
248
+ heads[nom] = heads[verb]
249
+ deprels[nom] = deprels[verb] if deprels[verb] != "PRED" else "root"
250
+ heads[verb] = nom
251
+ deprels[verb] = "cop"
252
+ for i in range(1, n + 1):
253
+ if i != nom and heads[i] == verb:
254
+ heads[i] = nom
255
+
256
+ # Stage 4: everything else -- straight relabel, with a few UPOS/context-sensitive refinements.
257
+ for i in range(1, n + 1):
258
+ d = deprels[i]
259
+ if d in ("cc", "conj", "case", "cop", "dep", "obl"): # already converted above
260
+ continue
261
+ if d == "ATR":
262
+ deprels[i] = ("det" if native_upos[i] == "l"
263
+ else "amod" if native_upos[i] == "a"
264
+ else "nmod")
265
+ elif d == "ADV":
266
+ deprels[i] = ("advmod" if native_upos[i] == "d"
267
+ else "advcl" if native_upos[i] == "v" else "obl")
268
+ elif d in _SIMPLE_DEPREL:
269
+ deprels[i] = _SIMPLE_DEPREL[d]
270
+ # else: already UD-style (e.g. a second pass over an already-converted label) or
271
+ # unrecognized -- left as-is rather than silently dropped.
272
+
273
+ out = []
274
+ for i, w in enumerate(sent, start=1):
275
+ upos, feats = xpos_to_upos_feats(w["xpos"])
276
+ if deprels[i] == "mark" and upos == "CCONJ":
277
+ upos = "SCONJ" # refine c (CCONJ default) once we know it introduced a subordinate clause
278
+ out.append(dict(form=w["form"], lemma=w["lemma"], upos=upos, xpos=w["xpos"],
279
+ feats=feats, head=heads[i], deprel=deprels[i]))
280
+ return out
281
+
282
+
283
  # ---- edit-script machinery (tagger/edits.py, ported verbatim on strings) ---------------------
284
 
285
  GRAVE, ACUTE = "̀", "́"
 
578
 
579
  # ---------------------------------------------------------------- full decode
580
 
581
+ def decode(self, model_out, batch, use_lexicon: bool = True, ud: bool = False) -> list[list[dict]]:
582
  """model_out: a CharBertJointOutput (or the equivalent return_dict=False tuple).
583
  batch: the dict returned by __call__ (needs `_forms`).
584
+ -> per sentence, a list of dicts, word order.
585
+
586
+ Default (ud=False): the model's NATIVE Perseus/AGDT-style output --
587
+ {form, xpos, lemma, upos, head, deprel}, where `upos` is the Perseus single-letter
588
+ code (not UD) and `deprel` is a Prague-style relation (SBJ/OBJ/ATR/COORD/AuxP/...).
589
+
590
+ ud=True: Universal Dependencies-style output instead -- {form, lemma, upos (UD),
591
+ xpos (native tag, kept as-is per UD convention), feats (dict), head, deprel (UD)}.
592
+ See `to_ud()` for the conversion (a documented, but not 100%-official-treebank-exact,
593
+ port of the HamleDT/UD_Ancient_Greek-Perseus conversion methodology)."""
594
  word_mask = model_out.word_mask if hasattr(model_out, "word_mask") else model_out[-1]
595
  xpos_logits = model_out.xpos_logits if hasattr(model_out, "xpos_logits") else model_out[0]
596
  script_logits = model_out.script_logits if hasattr(model_out, "script_logits") else model_out[1]
 
622
  if labels_out is not None and self.deprel_vocab else None)
623
  sent_out.append(dict(form=form, xpos=xpos, lemma=lemma, upos=upos,
624
  head=head, deprel=deprel))
625
+ results.append(to_ud(sent_out) if ud else sent_out)
626
  return results