Files
C.E.L_Slide_test2/tests/matching/pipeline_08_v2_semantic_rerank.py
KyeongminandClaude Opus 4.8 b836e79ee1 wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규
- Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가
- templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신
- tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분)
- tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가
- docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서
- scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸
- .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외

미완성 작업의 보존용 스냅샷 커밋 (2026-07-02)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:03:42 +09:00

216 lines
7.5 KiB
Python

"""Pipeline Step 8 — V2: V1 Top-K semantic rerank
파이프라인 위치:
V1 (pipeline_06_2_mdx_matching.py) → **V2 (이 스크립트)** → V3 → V4
설계 원칙:
- V1 결과(mdx_matching_result.yaml) 는 **고정 baseline**. 건드리지 않음.
- V1 rank_by_matching_score Top-K 만 rerank 대상 — K=5 (기준 잠금).
- 가중합 아님. **순수 rerank** — V1 점수는 저장만, 재정렬 키는 의미 유사도뿐.
- AI 판단 없음. ko-sroberta 임베딩은 결정론적 (seed 고정 가정).
입력:
- mdx_matching_result.yaml (V1)
- MDX_SECTIONS 원문 (pipeline_01 의 MDX_DIR + section config)
- Figma frame content (phase_common.load_32_frames() → analysis.md '내용')
처리:
1. 각 MDX 섹션(TARGET 4 + Holdout 3)별로:
- V1 Top-5 frame_id 추출
- MDX summary = detect_mdx.build_summary (title + 첫 문단 + slot labels)
- Top-5 frame.content ↔ summary 의 ko-sroberta cosine 계산
- cosine 내림차순으로 재정렬
2. lock_snapshot 기록 (V1 yaml + 이 스크립트 + 의존 모듈 sha256)
출력:
- v2_semantic_rerank_result.yaml
"""
import hashlib
import sys
import datetime
from pathlib import Path
import yaml
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
from phase_common import load_32_frames, load_frame_index
from detect_mdx import detect_mdx_analysis
from embeddings import embed_texts, cosine
from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR
TOP_K = 5
MODEL_ID = 'jhgan/ko-sroberta-multitask'
# ============================================================
# 상수 — lock_snapshot 대상
# ============================================================
LOCK_SNAPSHOT_FILES = [
'pipeline_08_v2_semantic_rerank.py',
'embeddings.py',
'detect_mdx.py',
'phase_common.py',
'pipeline_01_extract_nodes.py',
'synonyms.yaml',
'mdx_matching_result.yaml',
]
def sha256_file(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def extract_mdx_raw_section(section_id: str) -> tuple[str, str]:
"""(title, body_text) 반환. title = 첫 줄 heading 마커 제거, body_text = 원문 그대로."""
cfg = MDX_SECTIONS[section_id]
p = MDX_DIR / cfg['file']
lines = p.read_text(encoding='utf-8').split('\n')
start_idx = None
for i, ln in enumerate(lines):
if ln.strip() == cfg['start'].strip():
start_idx = i
break
if start_idx is None:
raise ValueError(f"start_heading 찾지 못함: {cfg['start']!r} in {p}")
end_idx = len(lines)
if cfg.get('end_prefix'):
for i in range(start_idx + 1, len(lines)):
if lines[i].strip().startswith(cfg['end_prefix']):
end_idx = i
break
section_lines = lines[start_idx:end_idx]
raw = '\n'.join(section_lines)
# title = heading 마커(#, ##, ###) 제거한 첫 줄
title_line = section_lines[0].lstrip('#').strip()
return title_line, raw
def main():
# 1. V1 결과 로드
v1_path = HERE / 'mdx_matching_result.yaml'
v1 = yaml.safe_load(v1_path.read_text(encoding='utf-8'))
# 2. Figma frame 로드 + content 임베딩 (한 번만)
frames = load_32_frames()
idx_data, frame_to_short = load_frame_index()
fids = list(frames.keys())
frame_contents = [frames[fid].get('content', '') for fid in fids]
fid_to_idx = {fid: i for i, fid in enumerate(fids)}
print(f"[V2] ko-sroberta 모델 로드 + 32 frame content 임베딩...")
frame_vecs = embed_texts(frame_contents)
# 3. 섹션별 처리
out_sections = {}
for sid, sec in v1['mdx_sections'].items():
top = sec['rank_by_matching_score'][:TOP_K]
top_fids = [r['frame_id'] for r in top]
# MDX summary 생성
title, raw_text = extract_mdx_raw_section(sid)
analysis = detect_mdx_analysis(raw_text, title, anchor_vocab=None)
summary = analysis['summary']
# summary 임베딩
mdx_vec = embed_texts([summary])[0]
# Top-K 에 대해서만 cosine 계산
rerank = []
for v1_rank_idx, r in enumerate(top, start=1):
fid = r['frame_id']
frame_idx = fid_to_idx[fid]
sem = cosine(mdx_vec, frame_vecs[frame_idx])
rerank.append({
'frame_id': fid,
'frame_number': r['frame_number'],
'v1_rank': v1_rank_idx,
'v1_score': r['matching_score'],
'semantic_score': round(float(sem), 4),
})
# semantic_score 내림차순 재정렬
rerank.sort(key=lambda x: -x['semantic_score'])
for new_rank, item in enumerate(rerank, start=1):
item['v2_rank'] = new_rank
out_sections[sid] = {
'section_type': sec.get('section_type'),
'answer_frame_number': sec.get('answer_frame_number'),
'mdx_title': title,
'mdx_summary': summary,
'top_k': TOP_K,
'v1_top_k': [
{
'rank': i + 1,
'frame_id': r['frame_id'],
'frame_number': r['frame_number'],
'v1_score': r['matching_score'],
}
for i, r in enumerate(top)
],
'v2_rerank': rerank,
}
# 4. lock_snapshot
lock = {
'timestamp': datetime.datetime.now().isoformat(timespec='seconds'),
'model': MODEL_ID,
'top_k': TOP_K,
'files': {
name: sha256_file(HERE / name) for name in LOCK_SNAPSHOT_FILES
},
'principle': [
'V1 baseline 고정 — 재정렬 대상만 Top-K 로 한정',
'가중합 금지 — 순수 semantic rerank',
'Holdout 성적을 설계 근거로 사용하지 않음',
],
}
out = {
'meta': {
'pipeline_step': '8.v2',
'description': 'V1 Top-K 후보를 ko-sroberta cosine 으로 재정렬 (캐스케이드 rerank 1단)',
'model': MODEL_ID,
'similarity': 'cosine',
'top_k': TOP_K,
'mdx_summary_spec': 'title + 첫 일반 문단(≤120자) + slot label Top-5 join (detect_mdx.build_summary)',
'frame_source': 'analysis.md "내용" 섹션 (phase_common.load_32_frames)',
'v1_source': 'mdx_matching_result.yaml rank_by_matching_score',
'holdout_sections': v1['meta']['holdout_sections'],
'answer_map': v1['meta']['answer_map'],
'lock_snapshot': lock,
},
'mdx_sections': out_sections,
}
out_path = HERE / 'v2_semantic_rerank_result.yaml'
out_path.write_text(
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
encoding='utf-8',
)
# 콘솔 요약
print()
print("=" * 70)
print(f"V2 재정렬 완료: {out_path}")
print("=" * 70)
answer_map = v1['meta']['answer_map']
for sid, s in out_sections.items():
answer_num = answer_map.get(sid)
v1_top1 = s['v1_top_k'][0]['frame_number']
v2_top1 = s['v2_rerank'][0]['frame_number']
v2_top1_v1rank = s['v2_rerank'][0]['v1_rank']
mark = ''
if answer_num is not None:
v1_ok = '✓' if v1_top1 == answer_num else '✗'
v2_ok = '✓' if v2_top1 == answer_num else '✗'
mark = f" 정답={answer_num} V1{v1_ok} V2{v2_ok}"
else:
mark = f" (holdout)"
print(f" [{sid:8}] V1 top1=Frame {v1_top1:>2} → V2 top1=Frame {v2_top1:>2} (V1 rank {v2_top1_v1rank}){mark}")
print()
if __name__ == '__main__':
main()