# 한국어 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` | 단위테스트 (외부 다운로드 불필요) | --- ## 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:` (또는 `--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 직렬화.