File size: 2,235 Bytes
99297d7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | """Notation-aware error classes for same-reading kana-kanji conversions.
All candidates in a pool convert the same kana input, so a wrong choice either spells
the same words differently (notation, A) or picks different words (semantic, S).
Conservative A: equal UniDic lemma sequences AND nested kanji sets. Against 210 earlier
hand-tagged V2.1/V4.2 flips this never labels a semantic flip as A (72/72) and recovers
118/138 notation flips; satisfying only one rule is reported as an upper bound ('a').
Requires `fugashi` and `unidic-lite` (requirements/evaluation.txt).
"""
import unicodedata
import fugashi
_TAGGER = None
_SKIP = ('補助記号', '空白')
ERROR_CLASSES = ('correct', 'pool_miss', 'notation', 'notation_upper', 'semantic')
def _tagger():
global _TAGGER
if _TAGGER is None:
_TAGGER = fugashi.Tagger()
return _TAGGER
def nfkc(text):
return unicodedata.normalize('NFKC', text)
def kanji(text):
return {c for c in text if '一' <= c <= '鿿' or c in '々〆ヶ'}
def kana_share(text):
letters = [c for c in text if c.isalpha()]
return sum('ぁ' <= c <= 'ヿ' for c in letters) / len(letters) if letters else 0.0
def lemmas(text):
return [w.feature.lemma or w.surface for w in _tagger()(nfkc(text)) if w.feature.pos1 not in _SKIP]
def relation(choice, answer):
"""'=' exact, 'A' notation (conservative), 'a' notation by one rule only, 'S' semantic."""
if choice == answer:
return '='
same_lemmas = lemmas(choice) == lemmas(answer)
nested = kanji(choice) <= kanji(answer) or kanji(answer) <= kanji(choice)
if same_lemmas and nested:
return 'A'
return 'a' if same_lemmas or nested else 'S'
def error_class(row, choice):
"""Class of a Top-1 choice and the closest reachable reference ('answer' is None if unreachable)."""
answers = row['answers']
if choice in answers:
return 'correct', choice
pool = {c['text'] for c in row['candidates']}
reachable = [a for a in answers if a in pool]
if not reachable:
return 'pool_miss', None
rank, answer = min(('=AaS'.index(relation(choice, a)), a) for a in reachable)
return {1: 'notation', 2: 'notation_upper', 3: 'semantic'}[rank], answer
|