70 lines
2.6 KiB
Python
70 lines
2.6 KiB
Python
"""구조 분석 - 형태소 분석 + lemma 교집합 비율.
|
||
|
||
전임자 가이드: 의미분석(임베딩)과 별도로, 내용어 형태소 기본형(lemma) 단위 교집합 비율을
|
||
보면 "원본을 복붙하고 말투(어미/조사)만 바꾼 표절"을 결정적으로 탐지할 수 있다.
|
||
|
||
예) "갔다" / "가던" / "가버렸다" → lemma 모두 "가다"
|
||
"빼앗았다" / "빼앗는다" → lemma 모두 "빼앗다"
|
||
임베딩 유사도로는 어미 변경만으로도 점수가 떨어지지만, lemma 교집합은 그대로 잡힌다.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from collections import Counter
|
||
from functools import lru_cache
|
||
|
||
from kiwipiepy import Kiwi
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 내용어 태그 - 명사/동사/형용사/부사. (조사 JK*, 어미 EF/EC/EP 등 기능어는 제외)
|
||
_CONTENT_TAGS = ("NNG", "NNP", "VV", "VA", "MAG", "VV-R", "VV-I", "VA-R", "VA-I")
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def _get_kiwi() -> Kiwi:
|
||
logger.info("Initializing Kiwi morphological analyzer")
|
||
return Kiwi()
|
||
|
||
|
||
def extract_lemmas(text: str, min_length: int = 1) -> list[str]:
|
||
"""텍스트에서 내용어 lemma 리스트 반환 (등장 순서, 중복 포함)."""
|
||
if not text.strip():
|
||
return []
|
||
tokens = _get_kiwi().tokenize(text)
|
||
lemmas: list[str] = []
|
||
for t in tokens:
|
||
if not any(t.tag.startswith(prefix) for prefix in ("NNG", "NNP", "VV", "VA", "MAG")):
|
||
continue
|
||
# kiwi 0.18+ Token.lemma 가 기본형 제공. 형용사/동사도 "-다" 형태.
|
||
lemma = getattr(t, "lemma", None) or t.form
|
||
if len(lemma) >= min_length:
|
||
lemmas.append(lemma)
|
||
return lemmas
|
||
|
||
|
||
def lemma_overlap_ratio(query_lemmas: list[str], ref_lemmas: list[str]) -> float:
|
||
"""query 기준 다중집합 교집합 비율 = |Q ∩ R (다중집합)| / |Q|.
|
||
|
||
한쪽 기준 비율을 쓰는 이유:
|
||
자카드(|Q∩R|/|Q∪R|)는 레퍼런스가 query보다 훨씬 길 때 점수가 깎인다.
|
||
"검사 대상이 레퍼런스에서 얼마나 가져왔는가"를 묻는 게 표절 판정 목적이므로
|
||
query 기준 precision-side 비율이 더 정확.
|
||
"""
|
||
if not query_lemmas:
|
||
return 0.0
|
||
qc = Counter(query_lemmas)
|
||
rc = Counter(ref_lemmas)
|
||
intersection = sum((qc & rc).values())
|
||
total = sum(qc.values())
|
||
return intersection / total if total else 0.0
|
||
|
||
|
||
def lemma_jaccard(query_lemmas: list[str], ref_lemmas: list[str]) -> float:
|
||
"""양방향 자카드. 보조 지표용."""
|
||
sq, sr = set(query_lemmas), set(ref_lemmas)
|
||
if not sq and not sr:
|
||
return 0.0
|
||
return len(sq & sr) / max(1, len(sq | sr))
|