docs: rewrite plagiarism data prep and preprocessing in certificate format

성적서 3.2.2.3 의 '1. 데이터 준비' 는 비표절/표절 예시를
{line_number, original_text} 형태로 첫 건과 500번째 건만 싣고 총 건수를
적는다. '2. 전처리 알고리즘' 은 코드와 예시를 나란히 둔다. 그 서식에 맞춘다.

데이터 예시는 testset_v2 의 실제 레코드에서 뽑았고, 전처리 예시는
컨테이너에서 단계별로 실행한 실제 출력이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-09-16 15:33:45 +09:00
parent 09647a3958
commit a758423b26

View File

@ -335,11 +335,21 @@ trainer.train()
**비표절 데이터 예시 :**
```
"pair_id": "legit-0001",
"text": "(인덱스에 포함되지 않은 작성자의 원문 — 게재용 예시는 익명화 완료 자료로 대체)"
```json
{
"line_number": 1,
"original_text": "할아버지는 마을에서 존경받는 인물이셨다. 면장을 역임하셨던 할아버지는 마을의
문제를 해결하고 주민들에게 존경받았다. 그리고 우리가족은 방앗간을 운영했다. 그러나 할아버지가
쌓아 올린 신뢰와 존경의 기반은 아버지의 사업 문제로 인해 큰 어려움을 겪게 되었다. …",
},
...
{
"line_number": 500,
"original_text": "나의 이름의 유래는 내가 성별에 따라 이름이 달라질 뻔했다는 이야기에서
시작된다. 아직도 전통적인 정서를 따라 이름에 '승' 자가 들어가야 했기에, 다른 사촌들과 겹치지
않는 이름을 고민했을 것이다. 부모님께 어떻게 이름을 지었는지 물었을 때, 철학관에서 이름을
지었다 …",
},
```
```
@ -350,15 +360,21 @@ trainer.train()
**표절 데이터 예시 :**
```
"pair_id": "plag-verbatim-0067",
"transformation": "verbatim",
"original_excerpt": "저는 어렸을 때부터 연애를 여러 번 해봤습니다. 어린 시절의 연애는 큰 의미
없이 가볍게 시작했었죠. 하지만 고등학교에 진학하고 나서 시작한 연애는 …",
"derived_text": "저는 어렸을 때부터 연애를 여러 번 해봤습니다. 어린 시절의 연애는 큰 의미
없이 가볍게 시작했었죠. 하지만 고등학교에 진학하고 나서 시작한 연애는 …"
```json
{
"line_number": 1,
"original_text": "저는 어렸을 때부터 연애를 여러 번 해봤습니다. 어린 시절의 연애는 큰 의미
없이 가볍게 시작했었죠. 하지만 고등학교에 진학하고 나서 시작한 연애는 느낌이 달랐습니다.
고등학교에 올라와서 처음으로 제대로 된 연애를 했습니다. 고1 때부터 친구의 친구로 알고 지내던
사이 …",
},
...
{
"line_number": 500,
"original_text": "이제 2020년입니다. 그와 동시에 코로나가 터졌습니다. 코로나는 대학 생활에
대한 저의 기대를 모두 박살 냈지만, 새로운 기회를 주기도 했습니다. 코로나 덕분에 시간이 많이
생겨 온라인 상담을 다녔고 국가 근로를 신청할 수 있게 되었고 아르바이트를 하며 돈을 모을 수 …",
},
```
```
@ -388,57 +404,96 @@ trainer.train()
**코드**
```python
# 공통 표현 제거 — 매칭된 자리를 공백으로 치환
def remove_common_patterns(text: str, patterns_path: str) -> str:
"""1단계 — 자서전 공통 표현 제거. 매칭된 자리를 공백으로 치환한다."""
result = text
for p in _common_patterns(patterns_path):
result = result.replace(p, " ")
return re.sub(r"\s+", " ", result).strip()
# 개체명 마스킹 — 인명/지명/날짜/숫자
_DATE_PATTERN = re.compile(r"\b(19|20)\d{2}\s*년|\d{1,2}\s*월\s*\d{1,2}\s*일|\d{4}-\d{2}-\d{2}")
_NUM_PATTERN = re.compile(r"\b\d{2,}\b")
def mask_entities(text: str) -> str:
"""2단계 — 개체명 마스킹. 날짜/숫자를 먼저 치환한 뒤 고유명사를 마스킹한다."""
masked = _DATE_PATTERN.sub("[DATE]", text)
masked = _NUM_PATTERN.sub("[NUM]", masked)
tokens = _kiwi().tokenize(masked) # 형태소 분석
parts = [(t.start, t.start + t.len, "[PERSON]")
for t in tokens if t.tag == "NNP"] # 고유명사
if not parts:
return masked
parts.sort(key=lambda p: p[0], reverse=True) # 뒤에서부터 치환
parts.sort(key=lambda p: p[0], reverse=True) # 뒤에서부터 치환 (인덱스 보존)
chars = list(masked)
for start, end, repl in parts:
chars[start:end] = list(repl)
return "".join(chars)
# 내용어 lemma 추출 — 어미·조사 변경형을 기본형으로 환원
# 내용어 태그 — 명사/동사/형용사/부사. 조사·어미 등 기능어는 제외한다.
_CONTENT_TAGS = ("NNG", "NNP", "VV", "VA", "MAG")
def extract_lemmas(text: str, min_length: int = 1) -> list[str]:
"""3단계 — 내용어 기본형(lemma) 추출.
어미·조사만 바꾼 표절을 잡기 위해 기본형으로 환원한다.
예) 갔다 / 가던 / 가버렸다 → 가다
"""
tokens = _get_kiwi().tokenize(text)
lemmas = []
for t in tokens:
if not any(t.tag.startswith(p) for p in ("NNG", "NNP", "VV", "VA", "MAG")):
if not any(t.tag.startswith(p) for p in _CONTENT_TAGS):
continue
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|.
"검사 대상이 레퍼런스에서 얼마나 가져왔는가" 를 묻는 것이 표절 판정 목적이므로
양방향 자카드 대신 한쪽 기준 비율을 쓴다.
"""
qc, rc = Counter(query_lemmas), Counter(ref_lemmas)
intersection = sum((qc & rc).values())
total = sum(qc.values())
return intersection / total if total else 0.0
```
**예시**
```
[전처리 전] 1978년 3월, 나는 춘천초등학교에 입학했다. 어머니께서 지어주신 새 옷을 입고 교문을 들어섰다.
[전처리 후] [DATE] 3월, 나는 [PERSON] . 어머니께서 지어주신 새 옷을 입고 교문을 들어섰다.
[lemma ] ['어머니', '짓다', '옷', '입다', '교문', '들어서다']
[원문]
1978년 3월, 나는 춘천초등학교에 입학했다. 할아버지는 마을에서 존경받는 인물이셨다.
면장을 역임하셨던 할아버지는 주민들에게 존경받았다. 우리 가족은 방앗간을 운영했다.
[1단계 공통 표현 제거]
1978년 3월, 나는 춘천 . 할아버지는 마을에서 존경받는 인물이셨다. 면장을 역임하셨던
할아버지는 주민들에게 존경받았다. 우리 가족은 방앗간을 운영했다.
[2단계 개체명 마스킹]
[DATE] 3월, 나는 [PERSON] . 할아버지는 마을에서 존경받는 인물이셨다. 면장을 역임하셨던
할아버지는 주민들에게 존경받았다. 우리 가족은 방앗간을 운영했다.
[3단계 내용어 lemma 추출]
['할아버지', '마을', '존경', '받다', '인물', '면장', '역임', '할아버지', '주민',
'존경', '받다', '가족', '방앗간', '운영']
```
연월일이 `[DATE]`, 고유명사 "춘천초등학교" 가 `[PERSON]` 으로 치환되고,
자서전 빈출 표현인 "에 입학했다" 가 공통 표현 사전에 걸려 제거되었다.
남은 내용어는 어미·조사를 떼고 기본형(`짓다`, `입다`, `들어서다`)으로 환원된다.
자서전 빈출 표현인 "초등학교에 입학했다" 가 1단계에서 제거되고, 연월일이 `[DATE]`,
고유명사 "춘천" 이 `[PERSON]` 으로 치환된다. 3단계에서는 어미·조사를 떼고 기본형
(`받다`)으로 환원하여, 말투만 바꾼 표절이 동일 lemma 로 잡히도록 한다.
> **전처리를 두는 이유** — 자서전은 입학·결혼·군 입대 등 공통 경험 서술이 많아
> 표면 유사도가 자연스럽게 높아진다. 전처리 없이 비교하면 서로 무관한 원고끼리도
> 유사도가 올라 거짓 양성이 발생한다.
#### 3. 표절 여부 판별