feat: query life writing at segment level
생활수기는 문서 단위로만 검사되고 있었다. 코퍼스에는 1694개 세그먼트가 적재돼 있는데 질의는 12개 문서를 통째로 던지는 방식이어서, 한 대목이 겹쳐도 글 전체 길이에 희석돼 유사도가 0.28~0.33 에 머물렀다. 그 상태의 미탐지 0건은 근거가 되지 못한다. --life-writing-segments 를 주면 자서전과 같은 세그먼트 단위로 펼친다. 검사 대상 7786건(자서전 6092 + 생활수기 1694) 으로 재실행한 결과 생활수기 침해의심은 0건이다. 다만 이번 0건은 근거가 있다. 결합유사도 최대 0.5745, 평균 0.3677 로 임계값 0.65 에 못 미치고 0.6 이상도 없다. 연속일치·커버리지 조건을 충족한 건도 없다. 자서전 결과는 106건으로 변화 없다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4e650d59ad
commit
6b27fff509
7786
reports/batch_with_life_writing.jsonl
Normal file
7786
reports/batch_with_life_writing.jsonl
Normal file
File diff suppressed because it is too large
Load Diff
@ -40,7 +40,7 @@ def build_or_load(store, index_dir: Path, model_name: str, device: str, batch: i
|
|||||||
return vectors, meta, model
|
return vectors, meta, model
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
p=argparse.ArgumentParser(); p.add_argument("--database",type=Path,required=True); p.add_argument("--index-dir",type=Path,required=True); p.add_argument("--out-jsonl",type=Path,required=True); p.add_argument("--out-xlsx",type=Path,required=True); p.add_argument("--first-jsonl",type=Path,required=True); p.add_argument("--model",default="BM-K/KoSimCSE-roberta-multitask"); p.add_argument("--batch-size",type=int,default=64); p.add_argument("--progress-every",type=int,default=25); p.add_argument("--exclude-segments",type=Path,help="검사·색인에서 뺄 segment_id 목록 파일 (중복 제거용)"); p.add_argument("--min-exact-span",type=int,help="연속 일치 판정 기준(공백 포함 글자 수). 미지정 시 설정값"); a=p.parse_args()
|
p=argparse.ArgumentParser(); p.add_argument("--database",type=Path,required=True); p.add_argument("--index-dir",type=Path,required=True); p.add_argument("--out-jsonl",type=Path,required=True); p.add_argument("--out-xlsx",type=Path,required=True); p.add_argument("--first-jsonl",type=Path,required=True); p.add_argument("--model",default="BM-K/KoSimCSE-roberta-multitask"); p.add_argument("--batch-size",type=int,default=64); p.add_argument("--progress-every",type=int,default=25); p.add_argument("--exclude-segments",type=Path,help="검사·색인에서 뺄 segment_id 목록 파일 (중복 제거용)"); p.add_argument("--min-exact-span",type=int,help="연속 일치 판정 기준(공백 포함 글자 수). 미지정 시 설정값"); p.add_argument("--life-writing-segments",action="store_true",help="생활수기도 자서전과 같이 세그먼트 단위로 검사"); a=p.parse_args()
|
||||||
logging.basicConfig(level=logging.INFO,format="%(asctime)s %(levelname)s %(message)s")
|
logging.basicConfig(level=logging.INFO,format="%(asctime)s %(levelname)s %(message)s")
|
||||||
if os.getenv("USE_LLM_LEGAL_JUDGE", "").lower() not in ("", "0", "false", "no", "off"): raise RuntimeError("USE_LLM_LEGAL_JUDGE=false required")
|
if os.getenv("USE_LLM_LEGAL_JUDGE", "").lower() not in ("", "0", "false", "no", "off"): raise RuntimeError("USE_LLM_LEGAL_JUDGE=false required")
|
||||||
import torch
|
import torch
|
||||||
@ -54,7 +54,7 @@ def main():
|
|||||||
if a.min_exact_span:
|
if a.min_exact_span:
|
||||||
settings=settings.model_copy(update={"persistent_min_exact_span":a.min_exact_span})
|
settings=settings.model_copy(update={"persistent_min_exact_span":a.min_exact_span})
|
||||||
LOG.info("판정 기준: 유사도>=%.2f | 연속일치>=%d자 | 커버리지>=%.2f",SCORE_MIN,span_min,COVERAGE_MIN)
|
LOG.info("판정 기준: 유사도>=%.2f | 연속일치>=%d자 | 커버리지>=%.2f",SCORE_MIN,span_min,COVERAGE_MIN)
|
||||||
detector=PlagiarismDetector(settings); groups=store.document_source_groups(); records=store.get_segments(meta["segment_ids"]); items=build_query_plan(store)
|
detector=PlagiarismDetector(settings); groups=store.document_source_groups(); records=store.get_segments(meta["segment_ids"]); items=build_query_plan(store,life_writing_segments=a.life_writing_segments)
|
||||||
# 중복 제거: 검사 대상과 색인 후보에서 동시에 뺀다. 한쪽만 빼면 지운 원고가
|
# 중복 제거: 검사 대상과 색인 후보에서 동시에 뺀다. 한쪽만 빼면 지운 원고가
|
||||||
# 여전히 상대로 잡혀 중복이 결과에 남는다.
|
# 여전히 상대로 잡혀 중복이 결과에 남는다.
|
||||||
if a.exclude_segments:
|
if a.exclude_segments:
|
||||||
|
|||||||
@ -59,7 +59,13 @@ def _reconstruct_document(segments: list[SegmentRecord]) -> str:
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def build_query_plan(store: CorpusStore) -> list[QueryItem]:
|
def build_query_plan(store: CorpusStore, *, life_writing_segments: bool = False) -> list[QueryItem]:
|
||||||
|
"""검사 대상 목록을 만든다.
|
||||||
|
|
||||||
|
생활수기는 기본이 문서 단위다. 다만 문서 전체를 한 번에 비교하면 한 대목이
|
||||||
|
통째로 겹쳐도 글 전체 길이에 희석되어 잡히지 않는다. 자서전과 같은 조건으로
|
||||||
|
검사하려면 life_writing_segments 로 세그먼트 단위로 펼친다.
|
||||||
|
"""
|
||||||
documents = {record.document_id: record for record in store.documents_with_metadata()}
|
documents = {record.document_id: record for record in store.documents_with_metadata()}
|
||||||
grouped: dict[str, list[SegmentRecord]] = defaultdict(list)
|
grouped: dict[str, list[SegmentRecord]] = defaultdict(list)
|
||||||
for segment in store.iter_segments():
|
for segment in store.iter_segments():
|
||||||
@ -73,6 +79,19 @@ def build_query_plan(store: CorpusStore) -> list[QueryItem]:
|
|||||||
).strip()
|
).strip()
|
||||||
source_kind = str(metadata.get("corpus_kind") or metadata.get("provenance") or "unknown")
|
source_kind = str(metadata.get("corpus_kind") or metadata.get("provenance") or "unknown")
|
||||||
segments = grouped.get(document_id, [])
|
segments = grouped.get(document_id, [])
|
||||||
|
if source_kind == "life_writing" and life_writing_segments:
|
||||||
|
for segment in segments:
|
||||||
|
if segment.text.strip():
|
||||||
|
items.append(QueryItem(
|
||||||
|
query_key=segment.segment_id,
|
||||||
|
document_id=document_id,
|
||||||
|
title=document.title,
|
||||||
|
source_group=source_group,
|
||||||
|
source_kind="life_writing",
|
||||||
|
text=segment.text,
|
||||||
|
source_segment_id=segment.segment_id,
|
||||||
|
))
|
||||||
|
continue
|
||||||
if source_kind == "life_writing":
|
if source_kind == "life_writing":
|
||||||
text = _reconstruct_document(segments).strip()
|
text = _reconstruct_document(segments).strip()
|
||||||
if text:
|
if text:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user