wl-ko-ner / span_postproc.py
m6dd8m's picture
switch to v5 + han-only drop
b718f01 verified
Raw
History Blame Contribute Delete
7.73 kB
"""
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} 톡과")