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:
parent
09647a3958
commit
a758423b26
@ -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. 표절 여부 판별
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user