o2o-plagiarism-ai/docs/AI_DETECTION.md
hbyang 91aa077898 docs: 과업 범위·성능지표 정리와 AI 의심도 위상 명시
SCOPE_AND_METRICS.md 신규. 연구개발계획서(2-4 성능지표, 나-3 개발내용,
나 성과물 목표)를 대조해 오투오 몫을 정리했다.

- 성능지표 8개 중 우리가 수치를 책임지는 것은 No.3·4·7 세 개다.
- No.4 는 재현율이 아니라 정밀도라 애매하면 표절이라 말하지 않는 쪽이
  지표에 유리하다. 이 성질이 설계를 지배한다.
- 사용자 맞춤형 요약의 상세도·강조 내용 옵션이 계획서에 있는데 미구현이다.
- python 3.9 호환성 위험: 평가환경이 3.9 고정인데 config.py 와 schemas.py 가
  from __future__ import annotations 없이 PEP 604 를 쓴다. 둘 다 Pydantic
  모델이라 어노테이션이 런타임에 평가된다. 현재 개발환경이 3.14 라 안 드러난다.
- No.3 귀속이 오투오/고려대 사이에서 불분명해 확인이 필요하다.

AI_DETECTION.md 에 「위상과 기준」절 추가. AI 생성 판별은 성능지표 8개
어디에도 없고 계획서 개발내용에도 없다. "콘텐츠 표절 여부 AI 탐지 모듈"은
AI로 표절을 탐지하는 모듈이지 AI가 쓴 글을 탐지하는 모듈이 아니다.
따라서 정확도를 보고하지 않는다. 정답 라벨이 없는 대상에 정확도를 주장하면
검토자를 과신하게 만들 뿐이다. 대신 근거(79권 대비 문체 이례도)와
기준(실측 백분위 컷)을 명시하고, 할 수 있는 말과 없는 말을 표로 구분했다.
8장의 예시 컷이 자리표시자 값이라 실측값으로 갱신했다.

DATA_REQUEST_SPEC.md §5 요약 정답셋 가이드 보강. 다중 참조 포맷, 줄글 규격,
dev/test 분리(test 는 사람 직접 작성 — LLM 이 쓴 정답을 LLM 출력으로 맞히면
점수가 부풀고 방어할 수 없다), book 단위 누출 방지, §5.1 파일럿 절차,
§5.2 recall 정정 이력.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 13:54:40 +09:00

397 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 한국어 AI 생성 의심도 (ai_detector)
> **이 기능이 산출하는 값은 "AI가 썼다"는 판정이 아니라, 사람 검토를 어디에 먼저
> 배정할지 정하는 우선순위 점수입니다.** 저자 통보·계약 조치·출간 거부의 단독
> 근거로 사용할 수 없습니다. 이유는 아래 [3. 한계와 오탐 위험](#3-한계와-오탐-위험)에
> 정리했습니다.
관련 파일:
| 파일 | 역할 |
|---|---|
| `app/engine/ai_detector.py` | 특징 추출 + 점수 산출 + 구간 채점 |
| `scripts/build_ai_training_dataset.py` | xlsx(human) + JSONL/CSV(AI) → 학습셋 |
| `scripts/train_ai_detector.py` | CPU 학습 + 보정 + 지표/아티팩트 저장 |
| `tests/test_ai_detector.py` | 단위테스트 (외부 다운로드 불필요) |
---
## 위상과 기준 — 이 기능은 성능지표가 아니다
### 위상
연구개발계획서의 성능지표 8개 어디에도 AI 생성 판별이 없다. 오투오 2단계 성과물인
"콘텐츠 표절 여부 AI 탐지 모듈"은 **AI로 표절을 탐지**하는 모듈이지 AI가 쓴 글을
탐지하는 모듈이 아니다. (`SCOPE_AND_METRICS.md` 참조)
따라서 이 기능은 **정확도·F1·AUROC를 보고하지 않는다.** 목표치도 두지 않는다.
공인인증 평가 대상이 아니며, 측정하지 않는 것이 누락이 아니라 설계다.
측정하지 않는 이유는 편의가 아니다. 정확도를 주장하려면 "이 원고는 AI가 썼다"는
정답 라벨이 있어야 하는데, 자서전 원고에 그런 라벨은 존재하지 않는다. 없는 정답으로
만든 수치는 검토자를 과신하게 만들 뿐이다.
### 근거
점수는 "AI일 확률"이 아니라 **보유 코퍼스의 인간 저작물 대비 문체 이례도**다.
- 기준 코퍼스: 79권 / 중복 제거 31,560개 에피소드 (전부 인간 저작)
- 채점: `ai_detector.py` 의 54개 한국어 문체 특징 (KatFishNet 계열, 품사 포함)
- 비교: 그 코퍼스 점수 분포에서의 백분위
이 진술은 **정확도를 측정하지 않아도 참이다.** "우리가 가진 인간 저작 79권과
비교했을 때 상위 2%에 드는 이례적 문체"라는 말에는 추정이 없다. 분포와 위치는
사실이기 때문이다. 반면 "AI가 썼을 확률 87%"는 근거 없는 주장이 된다.
### 기준
`scripts/calibrate_ai_detector_cuts.py` 로 산출한 백분위 컷이다.
| 항목 | 값 |
|---|---|
| low_cut (p90) | **0.4286** |
| high_cut (p98) | **0.5298** |
| 표본 | 3,000건 (79권에서 결정적 추출) |
| 품사 특징 | 사용함 (kiwipiepy) |
| OCR 정규화 | 적용 (`ocr_normalize.py`) |
| 인간 원고 기준 예상 medium 비율 | 7.0% |
| 인간 원고 기준 예상 high 비율 | 2.03% (설계 목표 2%) |
컷을 백분위로 잡은 두 번째 이유는 **검토 부하가 정의상 고정**된다는 것이다.
high 배지는 원고 100건당 약 2건에서 나온다. 모델이 바뀌어도 이 비율은 변하지
않으므로 검토 인력을 계획할 수 있다.
리포트 원본: `data/models/ai_cuts.json`
### 이 기능으로 할 수 있는 말과 없는 말
| 할 수 있는 말 | 할 수 없는 말 |
|---|---|
| "보유 인간 저작 코퍼스 대비 상위 2%의 이례적 문체" | "AI가 작성했다" / "AI 작성 확률 87%" |
| "검토 우선순위 상위" | "저자에게 통보할 근거" |
| "mixed — 구간별 점수가 갈려 부분 삽입 의심" | "이 챕터는 생성물이다" |
| "high 배지는 인간 원고에서도 2%가 나온다" | "high면 AI다" |
## 1. 동작 모드 — 점수를 지어내지 않는다
세 가지 모드가 있고, 결과 객체의 `is_stub` / `model_version` / `note` 로 **항상
구분 가능**합니다. 기존 `detector.py:_dummy_ai_generation_signal` 처럼 문자 코드
합으로 만든 임의 더미는 이 모듈에 없습니다.
| 모드 | 조건 | `available` | `score` | `is_stub` | `model_version` |
|---|---|---|---|---|---|
| `unavailable` | 아티팩트 없음 + `allow_heuristic=False` (**기본**) | `False` | `None` | `False` | `"unavailable"` |
| `heuristic` | 아티팩트 없음 + `allow_heuristic=True` | `True` | 0~1 | `True` | `"heuristic-baseline-v1"` |
| `trained` | 학습 아티팩트 로드 성공 | `True` | 0~1 (보정됨) | `False` | 예: `logreg-nolen-kf-ko-v1-auroc0.912` |
기본값이 `unavailable` 인 이유는, 검증되지 않은 점수가 조용히 운영 화면에 노출되는
상황을 막기 위해서입니다. 휴리스틱을 켜려면 명시적으로 opt-in 해야 합니다.
```bash
export AI_DETECTOR_MODEL_PATH=/mnt/data1/o2o/ai_detector/model.joblib
export AI_DETECTOR_ALLOW_HEURISTIC=false # 기본값
```
### 휴리스틱 baseline 에 대한 경고
`heuristic` 모드는 문헌상 알려진 경향(문장 길이 균일성, 쉼표 과다 등)을 선형
위치로 환산해 평균하는 규칙 계층입니다. **참조 구간 값은 아직 어떤 데이터로도
캘리브레이션되지 않은 자리표시자입니다.** 79권 인간 저작 코퍼스로 FPR을 측정하기
전까지는 데모·개발 편의용으로만 쓰고, 대외 산출물이나 사용자 화면에 노출하지
마십시오.
---
## 2. 특징 (`kf-ko-v1`)
KatFishNet 계열의 한국어 표지를 결정적·설명 가능하게 구현했습니다. 총
**54개**(기본 37 + 품사 17)이며 `FEATURE_NAMES` 순서가 곧 모델 입력 벡터의
순서입니다. 이 순서는
아티팩트에 저장되어 로드 시 대조되며, 불일치하면 **로드를 거부**합니다.
| 계열 | 대표 특징 | 근거 |
|---|---|---|
| 띄어쓰기 | `mean_eojeol_len`, `cv_eojeol_len`, `long_eojeol_ratio` | 생성문의 어절 길이 분포가 더 균일 |
| 문장 | `cv_sentence_len`, `std_sentence_len`, `mean_sentence_len` | **burstiness** — 사람 글은 문장 길이 기복이 큼 |
| 쉼표·부호 | `comma_per_sentence`, `comma_ratio`, `punct_diversity` | 생성문의 쉼표 과다 사용 경향 |
| 종결 | `end_da_ratio`, `end_yo_ratio`, `end_noun_ratio` | 종결어미 분포의 정형성 |
| 반복 | `char3gram_repeat_ratio`, `word_bigram_repeat_ratio`, `hapax_ratio` | 표현 재사용 |
| 품사 | `pos_bigram_entropy`, `pos_trigram_repeat_ratio`, `josa_diversity`, `eomi_diversity` | 품사 배열의 정형성, 조사·어미 다양도 |
| 길이 | `char_count`, `sentence_count`, `morph_count` 등 | 보조 — 아래 경고 참조 |
### 품사 특징 폴백
품사 특징은 `kiwipiepy` 가 있을 때만 산출됩니다. 미설치·초기화 실패·토큰화 예외
어느 경우에도 **예외를 밖으로 던지지 않고** 품사 특징을 0.0 으로 두며
`pos_available=0.0` 을 세웁니다. **특징 벡터의 길이와 순서는 절대 변하지
않습니다.** 모델이 품사로 학습됐는데(`requires_pos=True`) 실행 환경에 kiwi 가
없으면 `pos_required_by_model_but_missing` 경고가 붙습니다.
### ⚠️ 길이 특징 편향
human 에피소드와 AI 생성 표본의 **분량 분포가 다르면, 모델은 문체가 아니라
길이를 학습합니다.** 실제로 합성 데이터로 검증했을 때 상위 기여 특징이
`sentence_count`, `char_count`, `morph_count` 로 채워졌고,
`--drop-length-features` 를 켜자 `comma_ratio`, `josa_ratio`,
`std_sentence_len` 같은 실제 문체 지표로 바뀌었습니다.
**실데이터에서는 두 설정을 모두 돌려 성능 차이를 반드시 비교하십시오.** 길이를
빼도 성능이 유지되면 문체를 배운 것이고, 크게 떨어지면 길이만 보고 있었던
것입니다.
---
## 3. 한계와 오탐 위험
이 기능을 도입하기 전에 반드시 합의해야 할 항목입니다.
1. **자서전은 대필·윤문이 일상적입니다.** 편집자가 다듬은 원고는 문체가 정제되어
AI 쪽으로 기울기 쉽습니다. 이 도메인에서 FP는 "저자가 AI로 썼다"는 통보로
이어질 수 있고, 그건 명예 문제입니다.
2. **학습에 쓰이지 않은 생성 모델에는 일반화가 잘 되지 않습니다.** 특정 모델의
출력으로 학습하면 그 모델만 잡습니다.
3. **가벼운 수정으로 회피됩니다.** 문장을 몇 개 쪼개고 쉼표만 지워도 주요 특징이
흔들립니다.
4. **짧은 글은 통계가 성립하지 않습니다.** `HARD_MIN_CHARS=120` 미만은 채점을
거부하고, `MIN_RELIABLE_CHARS=300` 미만은 `short_text_low_confidence` 경고를
붙입니다.
5. **표절 점수와 절대 합산하지 마십시오.** 세 점수(표절 / 침해위험 / AI의심)는
끝까지 독립 필드여야 합니다.
### 평가 기준은 F1 이 아니라 FPR
인간 저작을 AI로 오판하는 비용이 압도적으로 크므로, 임계값은 F1 최적점이 아니라
**목표 FPR 상한**에서 잡습니다. `train_ai_detector.py` 의 `--target-fpr`(기본
0.05, `low_cut`)과 `--high-fpr`(기본 0.01, `high_cut`)이 그 역할입니다. 모델은
train에서 적합하고 임계값은 모델이 보지 않은 val에서 정하며, test는 최종 보고에만
사용합니다. val human 표본이 100건 미만이면 FPR 컷이 불안정하다는 경고가 납니다.
`low_cut == high_cut` 경고가 뜨면 medium 배지가 사라진 상태입니다. 표본이 너무
쉽게 분리되거나 표본 수가 부족하다는 신호이니 데이터를 재점검하십시오.
---
## 4. provenance (작성 경로 **추정**)
확정이 아니라 검토자 참고용 분류입니다. 구간 점수 분포에서 유도합니다.
| 값 | 규칙 | 검토자 해석 |
|---|---|---|
| `human` | 문서·구간 모두 low | 통상 검토 |
| `ai` | 문서 high, low 구간 없음 | 우선 검토 |
| `mixed` | high 구간과 low 구간이 공존 | **부분 삽입 의심** — 어느 구간인지 확인 |
| `edited` | 전 구간이 medium 에 몰림 | AI 초안 + 사람 윤문(또는 역) 가능성 |
| `unknown` | 채점 가능한 구간 없음 / 모델 미가용 | 판단 보류 |
`mixed` 가 실무상 가장 유용합니다. 문서 전체 점수 하나로는 "30만 자 중 한 챕터만
생성물"을 절대 못 잡지만, 구간 점수는 잡습니다.
---
## 5. 학습 절차
### 5.1 데이터 준비
```bash
# ① 컬럼 확인 (아무것도 쓰지 않음) — 반드시 먼저 실행
python scripts/build_ai_training_dataset.py --xlsx episodes.xlsx --inspect
# ② 빌드
python scripts/build_ai_training_dataset.py \
--xlsx episodes.xlsx \
--text-column "에피소드 본문" --book-column "도서명" \
--ai-jsonl data/training/ai_samples.jsonl \
--out data/training/ai_dataset.jsonl
```
**AI 샘플 JSONL 형식** (`--ai-text-field`/`--ai-generator-field` 로 변경 가능):
```json
{"text": "생성된 본문 …", "generator": "gpt-4o-mini", "book": ""}
```
### 5.2 누출 방지 — 이 설계의 핵심
분할 단위는 개별 텍스트가 아니라 **`source_group`** 입니다.
- human: `book:<도서명>` (도서 정보가 없으면 `sheet:<시트명>`)
- AI: `ai:<generator>` (또는 `--ai-group-field` 로 지정)
같은 책의 에피소드가 train 과 test 에 동시에 들어가면 모델이 문체가 아니라 **그
책을 외웁니다.** 빌더는 분할 후 그룹 중복을 검사해 발견 시 **exit 1** 로
실패하고, 학습 CLI도 train/val/test 그룹 중복을 확인해 **exit 2** 로 중단합니다.
분할은 해시 기반이라 결정적이며(`--seed`), 라벨별로 비율을 맞춥니다. 정규화 후
완전 중복 텍스트는 제거합니다(`recovered.csv` 에서 9,633행 중복이 나온 전례).
### 5.3 학습
```bash
python scripts/train_ai_detector.py \
--data data/training/ai_dataset.jsonl \
--model logreg \
--drop-length-features \
--target-fpr 0.05 --high-fpr 0.01 \
--out data/models/ai_detector.joblib
```
- `--model logreg` — StandardScaler + LogisticRegression. **기본 권장.** 계수 ×
표준화값으로 기여도를 뽑을 수 있어 검토자에게 근거를 보여줄 수 있습니다.
- `--model hgb` — HistGradientBoosting. 성능이 나을 수 있으나 기여도를 못 뽑아
`top_contributions` 가 빕니다(**지어내지 않습니다**).
- 두 경우 모두 `CalibratedClassifierCV`(sigmoid) + `StratifiedGroupKFold` 로
확률을 보정합니다. 보정 안 된 점수를 "의심도 %"로 띄우면 검토자가 과신합니다.
- GPU 불필요, 외부 API 호출 없음.
**즉시 실패하는 조건** (조용히 넘어가지 않고 exit 2):
label 필드 없음 / 단일 클래스 / 소수 클래스 10건 미만 / `source_group` 4개 미만 /
train·val·test 그룹 중복 / 어느 split이 단일 클래스 / train 그룹이 CV fold 보다 적음.
### 5.4 산출물
`model.joblib` 에는 estimator 와 함께 재현에 필요한 메타가 들어갑니다:
`feature_names`, `feature_set_version`, `model_version`, `requires_pos`,
`zeroed_features`, `low_cut`, `high_cut`, `metrics`, `sklearn_version`,
`trained_at`. `model.metrics.json` 에 AUROC / AUPRC / confusion matrix / FPR /
TPR / precision / recall / F1 이 train·validation·test 각각 저장됩니다.
---
## 6. 통합 연결점 (반영 완료)
아래 항목은 API 통합에 반영됐습니다. 변경 시 유지해야 할 계약으로 참고합니다.
### 6.1 `app/core/config.py`
```python
ai_detector_model_path: str = "./data/models/ai_detector.joblib"
ai_detector_allow_heuristic: bool = False
ai_detector_enabled: bool = True
```
Settings가 `get_ai_detector()`에 모델 경로·휴리스틱 허용·품사 사용 설정을 주입합니다.
### 6.2 `app/api/schemas.py` — `AiGenerationSignal` 확장
`AiGenerationSignal`에는 아래 필드가 반영돼 있습니다.
| 추가 필드 | 타입 | 비고 |
|---|---|---|
| `available` | `bool` | `False` 면 `score`/`suspicion_level` 이 `None` |
| `provenance` | `Literal["human","ai","mixed","edited","unknown"]` | 4절 |
| `model_version` | `str` | 감사 추적용 — 어느 모델이 낸 점수인지 |
| `feature_set_version` | `str` | `kf-ko-v1` |
| `warnings` | `list[str]` | `short_text_low_confidence` 등 |
| `segments` | `list[SegmentScore]` | 구간별 점수 |
| `top_contributions` | `list[{feature, contribution}]` | 설명 근거 |
| `pos_available` | `bool` | 품사 특징 사용 여부 |
`score`는 `None`을 허용하고 `suspicion_level=unknown`으로 unavailable 상태를 표현합니다.
### 6.3 `app/engine/detector.py`
문자 코드 합 더미는 제거됐고 전처리 전 raw text를 탐지기에 전달합니다.
```python
from app.engine.ai_detector import get_ai_detector
ai_result = get_ai_detector().detect(text)
```
`ReviewSummary.ai_suspicion_level`은 `unknown`을 지원하므로 미학습 결과를 `low`로
오독하지 않습니다.
단건 탐지는 threadpool에서 실행하고, 배치 경로는 구간 채점을 생략해 CPU 부하를 줄입니다.
### 6.4 바이칼 연동 문서
`docs/API_SPEC_BAIKAL.md` / `docs/API_GUIDE_BAIKAL.md`도 미학습 시 `unknown/null`을
반환하도록 갱신했습니다. **`is_stub=false`여도 확정 판정은 아닙니다.**
---
## 7. 테스트
```bash
python -m pytest tests/test_ai_detector.py -q
```
51건. **외부 모델 다운로드·네트워크 없이** 통과합니다. sklearn/kiwipiepy/joblib
미설치 환경에서도 핵심 경로는 전부 돌아가며(학습 모델 경로는 stub estimator 로
검증), joblib 이 있어야만 의미 있는 파일 아티팩트 3건만 skip 됩니다.
검증 항목: 특징 키 완전성·결정성·벡터 순서 계약 / 해시 더미가 아님(구조가 같으면
구조 특징도 같음) / kiwi 부재·토큰화 예외 폴백 / 길이특징 마스킹의 학습·추론
일치 / unavailable 시 점수 미산출 / 휴리스틱의 스텁 표기와 방향성 /
짧은 텍스트 거부·경고 / 구간 오프셋 유효성 / provenance 규칙 / 추론 실패 전파 /
특징 불일치 아티팩트 로드 거부 / JSON 직렬화.
---
## 8. 지표 없이 운영하기 — 백분위 컷 (권장 경로)
AI 생성 표본이 없어 정확도를 측정할 수 없을 때의 실용 경로다. **모델 학습도
성능지표도 없이** 규칙 점수만으로 검토 우선순위를 매긴다.
### 8.1 점수의 의미를 바꾼다
"AI일 확률"은 라벨 없이 측정할 수 없다. 대신 등록 코퍼스가 전부 인간 저작이라는
사실을 이용해, 그 점수 분포의 상위 백분위를 컷으로 잡는다. 그러면 결과는
> "등록 자서전 대비 상위 2% 이례적 문체 → 우선 검토"
가 되며, 이 진술은 **정확도를 측정하지 않아도 참**이다. 덤으로 `high` 배지 비율이
정의상 고정되어 검토 부하가 예측 가능해진다. 반면 "AI 확률 72%"는 어떤 근거로도
방어할 수 없다. 이 차이 때문에 백분위 컷을 권장한다.
### 8.2 절차
```bash
# 서버와 같은 환경(kiwipiepy 포함)에서 실행할 것 — 품사 특징 유무가 점수를 바꾼다
python scripts/calibrate_ai_detector_cuts.py \
--database data/runtime/corpus.sqlite3 --sample 3000
# 에피소드 xlsx 를 직접 코퍼스로 쓸 수도 있다 (OCR 정규화는 기본 켜짐)
python scripts/calibrate_ai_detector_cuts.py \
--xlsx <에피소드.xlsx> --sample 3000
```
출력된 두 줄을 `.env` 에 넣고 `AI_DETECTOR_ALLOW_HEURISTIC=true` 와 함께 재시작한다.
```dotenv
AI_DETECTOR_ALLOW_HEURISTIC=true
AI_DETECTOR_LOW_CUT=0.4286
AI_DETECTOR_HIGH_CUT=0.5298
```
> 위 값은 79권 코퍼스 3,000건 실측치다(`data/models/ai_cuts.json`). 코퍼스가
> 바뀌면 다시 뽑아야 한다.
`--low-percentile`(기본 90) / `--high-percentile`(기본 98) 로 배지 비율을 조절한다.
low/high 는 **둘 다** 설정해야 적용된다. 하나만 넣으면 자리표시자로 되돌아간다.
### 8.3 스크립트가 거부하는 경우
규칙 점수는 각 규칙이 [0,1] 로 clip 되므로 상단에서 **포화**할 수 있다. 포화가
심하면 p90 과 p98 이 같은 값이 되고, 그 컷을 쓰면 `high` 배지가 영원히 0건이 된다.
이 경우 스크립트는 `.env` 를 출력하지 않고 **exit 3** 으로 중단하며 진단을 남긴다
(`saturated_share`, `distinct_scores`). 대응은 `--high-percentile` 을 낮추거나,
표본을 늘리거나, 표본이 특정 도서에 치우쳤는지 확인하는 것이다.
유효 표본 100건 미만이면 백분위가 불안정하므로 **exit 2** 로 중단한다.
### 8.4 캘리브레이션해도 달라지지 않는 것
- `is_stub` 은 **여전히 `true`** 다. 컷을 맞췄을 뿐 학습된 모델이 아니다.
- `model_version` 만 `heuristic-baseline-v1` → `heuristic-baseline-v1+corpus-percentile`
로 바뀌어 컷의 출처를 드러낸다.
- AI 생성 텍스트를 실제로 구분한다는 근거는 **여전히 없다.** 이 점수가 높다는 것은
"등록된 인간 원고들과 문체 통계가 다르다"는 뜻일 뿐이며, 번역체·대필·윤문·장르
차이 모두가 같은 방향으로 점수를 올린다.
- 따라서 저자·편집자에게 노출할 때는 "AI 생성 의심도"보다 **"문체 이례도"** 로
표기하고, 검토 우선순위 정렬 용도로만 쓸 것을 권한다.
- 코퍼스가 OCR 후처리본이면 `--ocr-normalize`(기본 켜짐)로 조판 흔적을 지운 뒤
컷을 잡아야 한다. 그러지 않으면 컷이 저자 문체가 아니라 그 책의 조판 관습 위에서
잡힌다. 실측상 휴리스틱 점수 분포는 정규화 전후 차이가 작았지만(컷 동일),
학습 경로에서는 낫표·한자병기가 강한 지름길 특징이 되므로 반드시 적용한다.