File size: 7,734 Bytes
d787d0f b718f01 d787d0f b718f01 d787d0f b718f01 d787d0f | 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """
span_postproc.py β Q3 κ²½κ³μ€λ₯ CHEAP νμ²λ¦¬ (μ¬νμ΅ 0). litreview 2026-06-04 Β§3 + q3-boundary-fix-plan.
CHEAP-1: νκΈ(ζΌ’ε) μ£Όμ / μ§ μ λ§λ κ΄νΈ 꼬리λ₯Ό PS/OG/LC κ³ μ λͺ
μμ νΈλ¦¬λ°.
νκ΅μ΄ νΌμ©νκΈ° 'νκΈ(ζΌ’ε)' κ΄λ‘μ νμ μ£Όμμ mention μ λ―Έν¬ν¨ β 'μ¬μ(ιθ₯Ώ' β 'μ¬μ'.
μμ νμ κ³ μ λͺ
(νκΈ prefix μμ)μ 보쑴 β νκΈ prefix κ° μμ λλ§ νμ/κ΄νΈ 꼬리 μ κ±°.
CHEAP-3: type-aware stoplist (gold FP μλ, 보μμ ). μΌλ°μ΄/μ§μμ΄λ§, gold μ λ΅κ³Ό 무좩λμΈ μλ§.
λ¬Έλ§₯ μμ‘΄ μ΄ν(μ λΆ=μ€μ κΈ°κ΄μΌ μλ, ν 건=gold κ° QT λ‘ μν¨)λ *μ μΈ* β κ³Όμ΅μ λ°©μ§.
μ μ© μμΉ: λ°νμ/νκ° μΆμΆ *λ§μ§λ§* (idiom_filter λ€μ, canon+model+regex+idiom λ³ν© ν).
NER λͺ¨λΈ/regex/canon/silver λ μ 건λλ¦Ό β μμ μΆλ ₯ νμ²λ¦¬.
곡μ : νμ΄μ¬ νκ°κ²½λ‘(eval_full_pipelineΒ·gold_eval) + μλ²(hf-space/app.py) + (ν¬ν
μμ ) plugin ner.js.
app.py λ offset(start/end) λλ°μ΄λΌ trim_span_offsets λ‘ surface+offset λμ νΈλ¦¬λ°.
self-test: νμ¬ gold-multibot FP μΌμ΄μ€κ° μλλλ‘ νΈλ¦¬λ°/μ΅μ λλμ§.
μ€ν: py span_postproc.py
from span_postproc import postprocess, trim_affix, nfc
"""
import sys
import unicodedata
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
# ββ CHEAP-1: κ΄νΈ/νμ 꼬리 νΈλ¦¬λ° ββ
_OPEN = "οΌ(οΌ»[γγο½’γ{οΌ<"
_CLOSE = "οΌ)οΌ½]γγο½£γ}οΌ>"
def _is_hangul(c):
return "κ°" <= c <= "ν£"
def has_hangul(s):
"""λ¬Έμμ΄μ νκΈ μμ μ΄ νλλΌλ μλ β νμ-only/κ΄νΈ μμ¬(μ: pipeline μ΄ μͺΌκ° 'ιθ₯Ώ') νλ³μ©."""
return any(_is_hangul(c) for c in (s or ""))
def _is_cjk(c):
"""CJK ν΅ν©νμ(+νμ₯ A/νΈν). νκΈ prefix λ€ νμ μ£Όμ νμ μ©."""
o = ord(c)
return (0x3400 <= o <= 0x9FFF) or (0xF900 <= o <= 0xFAFF) or (0x20000 <= o <= 0x2FA1F)
def trim_affix(surface):
"""PS/OG/LC surface μ νμ μ£Όμ/κ΄νΈ 꼬리 μ κ±°. νκΈ prefix κ° μμ λλ§ μλ¦.
'μ¬μ(ιθ₯Ώ' β 'μ¬μ' Β· 'μ¬μ(ιθ₯Ώ)' β 'μ¬μ' Β· 'νκ°(ζΌ’ζ±)' β 'νκ°'
'μ€λ ¨ν' β 'μ€λ ¨ν' (λ³ν μμ) Β· 'ιθ₯Ώ' β 'ιθ₯Ώ' (μμ νμ 보쑴)
"""
s = (surface or "").strip()
if not s:
return s
# νκΈ prefix + (μ¬λκ΄νΈ | νμλΈλ‘) μμ μ§μ μμ μ»· (κ·Έ μμ νκΈμ΄ μ€μ¬ν λλ§)
if any(_is_hangul(c) for c in s):
for i, c in enumerate(s):
if (c in _OPEN or _is_cjk(c)) and any(_is_hangul(p) for p in s[:i]):
s = s[:i]
break
# μλμ λ¨μ κ΄νΈ/곡백 μ 리 (μ§ μ λ§λ μ¬λΒ·λ«λ κ΄νΈ)
s = s.strip().strip(_OPEN + _CLOSE).strip()
return s
def trim_span_offsets(text, start, end):
"""offset λλ° νΈλ¦¬λ° (app.py / plugin parity). (start, end, surface) λ°ν.
surface = text[start:end] λ₯Ό trim_affix ν λ€, μλ €λκ° λ§νΌ end λ₯Ό λΉκ²¨ offset μΌκ΄ μ μ§.
μμͺ½μ΄ μ리λ κ²½μ°(λ«λ κ΄νΈλ‘ μμ λ±)λ start λ μ μ§.
"""
surf = text[start:end]
trimmed = trim_affix(surf)
if trimmed == surf:
return start, end, surf
# trimmed λ surf μ μ°μ λΆλΆλ¬Έμμ΄ β μ/λ€ μλ¦Ό ν κ³μ°
off = surf.find(trimmed) if trimmed else 0
new_start = start + off
new_end = new_start + len(trimmed)
return new_start, new_end, trimmed
# ββ CHEAP-3: type-aware stoplist (보μμ ) ββ
# κ° μμ νμ¬ gold-multibot FP μ μ€μ¬ + gold μ λ΅κ³Ό 무좩λ. λ¬Έλ§₯ μμ‘΄μ΄λ μλμ μΌλ‘ μ μΈ.
STOP_PAIRS = {
("κ·Έλ ", "DT"), # μ§μ νν (κ΄΄μ΄νμ¬ 'κ·Έλ μ λ
')
("μλ°©", "PS"), # νΈμΉ 'μλ°©λ'
("νλ", "PS"), # μμ¬ 'λ¨ νλ' (gold λ νλ=QT λ‘ μν¨ β PS λ§ μ°¨λ¨)
("λ λ¬Έν", "QT"), # μΌλ° μλ+λͺ
μ¬ ('νμ°νμ μ’
λ¨ν = λ λ¬Έν')
("μ΄λμ μ", "PS"), # 무ν μ μ λ±κΈ (μΌλ° λͺ
μ¬, μΈλ¬Ό μλ)
}
def is_stopword(surface, typ):
return (surface, typ) in STOP_PAIRS
# ββ ν΅ν© νμ²λ¦¬ ββ
_TRIM_TYPES = ("PS", "OG", "LC") # νμ μ£Όμ κ΄λ‘λ κ³ μ λͺ
μλ§ (DT/TI/QT λ κ΄νΈ μ μ«μκ° μ λΉ)
def postprocess(preds):
"""[(surface, type), ...] β CHEAP-1 νΈλ¦¬λ° + CHEAP-3 stoplist μ μ©. idiom_filter λ€μ μ리.
νΈλ¦¬λ°μΌλ‘ λΉ λ¬Έμμ΄μ΄ λλ©΄ λλ‘. νΈλ¦¬λ°μ΄ λ§λ (surface,type) μ€λ³΅λ μ κ±°.
"""
out = []
seen = set()
for p in preds:
surf, typ = p[0], p[1]
if typ in _TRIM_TYPES:
s = trim_affix(surf)
if not has_hangul(s): # νΈλ¦¬λ° ν νκΈ 0 = νμ/κ΄νΈ μμ¬(pipeline μ΄ μͺΌκ° 'ιθ₯Ώ' λ±) β λλ‘
continue
else:
s = (surf or "").strip()
if not s:
continue
if is_stopword(s, typ):
continue
if (s, typ) in seen:
continue
seen.add((s, typ))
out.append((s, typ))
return out
def nfc(text):
"""CHEAP-4: μ
λ ₯/gold λ₯Ό NFC λ‘ ν΅μΌ (μ κ°Β·κ²°ν©λ¬Έμ offset μ΄κΈλ¨ λ°©μ§)."""
return unicodedata.normalize("NFC", text or "")
if __name__ == "__main__":
trim_cases = [
("μ¬μ(ιθ₯Ώ", "μ¬μ"),
("μ¬μ(ιθ₯Ώ)", "μ¬μ"),
("μ¬μ(", "μ¬μ"),
("νκ°(ζΌ’ζ±)", "νκ°"),
("μλ¨ ν΄", "μλ¨ ν΄"), # λ³ν μμ
("μ€λ ¨ν", "μ€λ ¨ν"), # λ³ν μμ
("ιθ₯Ώ", "ιθ₯Ώ"), # μμ νμ 보쑴 (νκΈ prefix μμ)
("μ€λ¦¬μν
μ΄μ
λ£Έ", "μ€λ¦¬μν
μ΄μ
λ£Έ"),
]
ok = 0
print("[trim] CHEAP-1 νμ/κ΄νΈ 꼬리 νΈλ¦¬λ°:")
for src, exp in trim_cases:
got = trim_affix(src)
flag = "OK" if got == exp else "XX"
ok += got == exp
print(f" [{flag}] trim_affix({src!r}) = {got!r} (κΈ°λ {exp!r})")
stop_cases = [
(("νλ", "PS"), True),
(("νλ", "QT"), False), # gold μ λ΅ β 보쑴
(("κ·Έλ ", "DT"), True),
(("μλ°©", "PS"), True),
(("λ λ¬Έν", "QT"), True),
(("μ΄λμ μ", "PS"), True),
(("μ λΆ", "OG"), False), # gold μ λ΅ β 보쑴
(("ν 건", "QT"), False), # gold μ λ΅ β 보쑴
]
print("\n[stop] CHEAP-3 type-aware stoplist:")
for (surf, typ), exp in stop_cases:
got = is_stopword(surf, typ)
flag = "OK" if got == exp else "XX"
ok += got == exp
print(f" [{flag}] is_stopword({surf!r}, {typ!r}) = {got} (κΈ°λ {exp})")
# ν΅ν©: μ¬μ( κ° νΈλ¦¬λ°λμ΄ gold μ λ§€μΉλλμ§ + offset νΈλ¦¬λ°
print("\n[post] ν΅ν© νμ²λ¦¬:")
pp = postprocess([("μ¬μ(", "LC"), ("ιθ₯Ώ", "LC"), ("νλ", "PS"), ("κ·Έλ ", "DT"), ("μλ¨ ν΄", "PS")])
exp_pp = [("μ¬μ", "LC"), ("μλ¨ ν΄", "PS")] # ιθ₯Ώ(νμonly)Β·νλ(PS)Β·κ·Έλ (DT) λλ‘
flag = "OK" if pp == exp_pp else "XX"
ok += pp == exp_pp
print(f" [{flag}] postprocess(...) = {pp} (κΈ°λ {exp_pp})")
s0, e0, surf0 = trim_span_offsets("μ¬μ(ιθ₯Ώ)μ 거리λ", 0, 5) # 'μ¬μ(ιθ₯Ώ' span
flag = "OK" if surf0 == "μ¬μ" and (s0, e0) == (0, 2) else "XX"
ok += surf0 == "μ¬μ" and (s0, e0) == (0, 2)
print(f" [{flag}] trim_span_offsets = ({s0},{e0},{surf0!r}) (κΈ°λ (0,2,'μ¬μ'))")
total = len(trim_cases) + len(stop_cases) + 2
print(f"\n[span_postproc] self-test {ok}/{total} ν΅κ³Ό")
|