o2o-plagiarism-ai/app/engine/structural.py

70 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""구조 분석 - 형태소 분석 + 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|/|QR|)는 레퍼런스가 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))