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>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
"""Phase 15 — 스토리텔링: "TF-IDF(키워드)는 왜 실패하고 AI-Meta(구조+의미)는 왜 성공하는가"
|
||||
MDX03-1 케이스로 Before/After 대조.
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import collections
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from common import load_figma_texts
|
||||
from extract_units import extract_units
|
||||
from methods import (
|
||||
_load_metadata_db, _build_tfidf, method_tfidf,
|
||||
method_ai_meta_for_uid,
|
||||
)
|
||||
|
||||
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
|
||||
PREVIEW_DIR = ROOT / "data" / "figma_previews"
|
||||
|
||||
|
||||
def main():
|
||||
units = extract_units()
|
||||
figma = load_figma_texts()
|
||||
meta = _load_metadata_db()
|
||||
mdx_sections = meta["mdx_sections"]
|
||||
figma_frames = meta["figma_frames"]
|
||||
|
||||
with open(PREVIEW_DIR / "index.json", encoding="utf-8") as f:
|
||||
idx_data = json.load(f)
|
||||
frame_to_short = {info["frame_id"]: sid for sid, info in idx_data.items()}
|
||||
png_rel = "../../data/figma_previews/"
|
||||
|
||||
# ═══ IDF 계산 (AI-Meta 용, Figma 개념 집합 기준) ═══
|
||||
df_concept = collections.Counter()
|
||||
N_concept = 0
|
||||
for fid, v in figma_frames.items():
|
||||
if isinstance(v, dict):
|
||||
for c in v.get("concepts", []):
|
||||
df_concept[c] += 1
|
||||
N_concept += 1
|
||||
idf_concept = {c: math.log(N_concept / cnt) for c, cnt in df_concept.items() if cnt > 0}
|
||||
|
||||
# ═══ 주인공: MDX03-1 ═══
|
||||
uid = "MDX03-1"
|
||||
correct_sid = "13"
|
||||
wrong_sid = "15" # TF-IDF가 1위로 고르는 오답
|
||||
correct_fid = "1171281190"
|
||||
wrong_fid = "1171281192"
|
||||
|
||||
lines = []
|
||||
lines.append("# Phase 15 — 왜 키워드만으론 실패하고, AI 구조+의미가 붙으니 성공하는가")
|
||||
lines.append("")
|
||||
lines.append("## 🎬 시나리오")
|
||||
lines.append("")
|
||||
lines.append("**MDX03-1** \"1. DX 시행을 위한 필수 요건\" (기술·사람·자연 3요소)")
|
||||
lines.append("")
|
||||
lines.append("**정답**: Frame 13 (3열 구조 \"필수조건\" — 기술/사람/자연 카드)")
|
||||
lines.append(f"**오답으로 자주 잡히는 후보**: Frame 15 (\"정책목표\" — 성장·비용절감·안전 4개 카드)")
|
||||
lines.append("")
|
||||
|
||||
# 썸네일
|
||||
lines.append("| 정답 | 오답(TF-IDF 1위) |")
|
||||
lines.append("|------|------------------|")
|
||||
c_png = idx_data[correct_sid]["png"]
|
||||
w_png = idx_data[wrong_sid]["png"]
|
||||
lines.append(f"| <br>**Frame {correct_sid}** — 필수조건 | "
|
||||
f"<br>**Frame {wrong_sid}** — 정책목표 |")
|
||||
lines.append("")
|
||||
|
||||
lines.append("두 프레임 모두 **3~4열 카드 배치** + 건설·DX 관련 용어가 등장 → 단어만 보면 구분이 어려움.")
|
||||
lines.append("")
|
||||
|
||||
# ═══ Part 1: TF-IDF 시도 ═══
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("# Part 1. 기존 방식 — TF-IDF (키워드 표면만 본다)")
|
||||
lines.append("")
|
||||
|
||||
# TF-IDF 실제 돌려서 top 3 얻기
|
||||
mdx_text = units[uid]
|
||||
ranked = method_tfidf(mdx_text, figma)[:3]
|
||||
|
||||
# TF-IDF 벡터 재계산해서 공통 단어 추출용
|
||||
all_docs = {"__mdx__": mdx_text, **figma}
|
||||
vecs, df, N = _build_tfidf(all_docs)
|
||||
mdx_vec = vecs["__mdx__"]
|
||||
|
||||
lines.append("### TF-IDF Top-3 결과")
|
||||
lines.append("")
|
||||
lines.append("| 순위 | Frame | 점수 | 결과 |")
|
||||
lines.append("|------|-------|------|------|")
|
||||
for rank, (fid, score) in enumerate(ranked, 1):
|
||||
sid = frame_to_short.get(str(fid), "?")
|
||||
mark = "✗ 오답(1위)" if rank == 1 and sid != correct_sid else ("⭐ 정답" if sid == correct_sid else "")
|
||||
lines.append(f"| {rank}위 | **{sid}** | {score * 100:.0f}% | {mark} |")
|
||||
lines.append("")
|
||||
|
||||
# 왜 Frame 15가 1위인지 — 공통 단어 분석
|
||||
lines.append("### 왜 Frame 15가 1위로 잘못 뽑혔나 — 공통 단어 분해")
|
||||
lines.append("")
|
||||
wrong_vec = vecs[wrong_fid]
|
||||
correct_vec = vecs[correct_fid]
|
||||
|
||||
def common_words(vec_a, vec_b, top=8):
|
||||
common = [(w, vec_a[w], vec_b[w], vec_a[w] * vec_b[w])
|
||||
for w in vec_a if w in vec_b]
|
||||
common.sort(key=lambda x: -x[3])
|
||||
return common[:top]
|
||||
|
||||
lines.append("**MDX03-1 ↔ Frame 15 (정책목표) — 공통 단어 상위 8개**")
|
||||
lines.append("")
|
||||
lines.append("| 단어 | MDX TF-IDF | Frame 15 TF-IDF | 기여도 |")
|
||||
lines.append("|------|------------|-----------------|--------|")
|
||||
for w, a, b, contrib in common_words(mdx_vec, wrong_vec):
|
||||
lines.append(f"| {w} | {a:.3f} | {b:.3f} | {contrib:.4f} |")
|
||||
lines.append("")
|
||||
|
||||
lines.append("**MDX03-1 ↔ Frame 13 (필수조건, 정답) — 공통 단어 상위 8개**")
|
||||
lines.append("")
|
||||
lines.append("| 단어 | MDX TF-IDF | Frame 13 TF-IDF | 기여도 |")
|
||||
lines.append("|------|------------|-----------------|--------|")
|
||||
for w, a, b, contrib in common_words(mdx_vec, correct_vec):
|
||||
lines.append(f"| {w} | {a:.3f} | {b:.3f} | {contrib:.4f} |")
|
||||
lines.append("")
|
||||
|
||||
lines.append("### TF-IDF가 실패하는 이유")
|
||||
lines.append("")
|
||||
lines.append("1. **단어 표면만 본다**: \"요건\"이라는 단어가 MDX(필수 **요건**)에도 있고 Frame 15(실행**요건**)에도 있음. 문맥·의미는 완전 다른데 TF-IDF는 그냥 같은 단어로 취급")
|
||||
lines.append("2. **Frame 13의 핵심 단어 \"필수조건\"이 깨진다**: Kiwi 같은 형태소 분석기는 \"필수조건\"을 \"필수\" + \"조건\"으로 분해. MDX는 \"필수 **요건**\"을 쓰는데 Frame 13은 \"필수**조건**\". \"요건\" ↔ \"조건\"은 동의어인데 글자가 달라 매칭이 안 됨")
|
||||
lines.append("3. **구조를 못 본다**: \"이 MDX는 3요소 병렬 설명이고 Frame 13도 3열 병렬 카드\"라는 구조 정보를 TF-IDF는 인지 불가. \"몇 열\", \"어떤 패턴\" 정보가 통째로 빠짐")
|
||||
lines.append("")
|
||||
lines.append("→ 결과: 표면 단어 겹침이 많은 Frame 15가 1위로, 진짜 같은 구조·의미인 Frame 13이 2위로 밀림")
|
||||
lines.append("")
|
||||
|
||||
# ═══ Part 2: AI-Meta로 해결 ═══
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("# Part 2. AI-Meta — AI가 구조와 의미를 개념 집합으로 정규화")
|
||||
lines.append("")
|
||||
|
||||
lines.append("### AI가 사전에 뽑아둔 개념 집합 (metadata_db.yaml)")
|
||||
lines.append("")
|
||||
mdx_concepts = mdx_sections[uid]["concepts"]
|
||||
mdx_layout = mdx_sections[uid].get("layout_hint", "")
|
||||
correct_concepts = figma_frames[correct_fid]["concepts"]
|
||||
wrong_concepts = figma_frames[wrong_fid]["concepts"]
|
||||
|
||||
lines.append(f"**MDX03-1** (layout_hint: `{mdx_layout}`)")
|
||||
lines.append("")
|
||||
lines.append(f"```yaml")
|
||||
lines.append(f"concepts: {mdx_concepts}")
|
||||
lines.append(f"```")
|
||||
lines.append("")
|
||||
lines.append(f"**Frame 13** (정답)")
|
||||
lines.append("")
|
||||
lines.append(f"```yaml")
|
||||
lines.append(f"concepts: {correct_concepts}")
|
||||
lines.append(f"```")
|
||||
lines.append("")
|
||||
lines.append(f"**Frame 15** (TF-IDF가 1위로 찍었던 오답)")
|
||||
lines.append("")
|
||||
lines.append(f"```yaml")
|
||||
lines.append(f"concepts: {wrong_concepts}")
|
||||
lines.append(f"```")
|
||||
lines.append("")
|
||||
|
||||
# AI-Meta 점수 계산
|
||||
ai_ranked = method_ai_meta_for_uid(uid, figma)[:3]
|
||||
lines.append("### AI-Meta Top-3 결과")
|
||||
lines.append("")
|
||||
lines.append("| 순위 | Frame | 점수 | 결과 |")
|
||||
lines.append("|------|-------|------|------|")
|
||||
for rank, (fid, score) in enumerate(ai_ranked, 1):
|
||||
sid = frame_to_short.get(str(fid), "?")
|
||||
mark = "⭐ 정답" if sid == correct_sid else ""
|
||||
lines.append(f"| {rank}위 | **{sid}** | {score * 100:.1f}% | {mark} |")
|
||||
lines.append("")
|
||||
|
||||
# 교집합 상세
|
||||
lines.append("### 왜 Frame 13이 1위가 됐나 — 개념 교집합")
|
||||
lines.append("")
|
||||
|
||||
mdx_set = set(mdx_concepts)
|
||||
correct_set = set(correct_concepts)
|
||||
wrong_set = set(wrong_concepts)
|
||||
|
||||
inter_c = mdx_set & correct_set
|
||||
inter_w = mdx_set & wrong_set
|
||||
|
||||
lines.append("**MDX03-1 ∩ Frame 13 (정답)**")
|
||||
lines.append("")
|
||||
lines.append("| 공통 개념 | Figma 출현 빈도 | IDF (희귀도) |")
|
||||
lines.append("|-----------|----------------|--------------|")
|
||||
inter_c_sorted = sorted(inter_c, key=lambda c: -idf_concept.get(c, 0))
|
||||
for c in inter_c_sorted:
|
||||
lines.append(f"| **{c}** | {df_concept.get(c, 0)}/32 | {idf_concept.get(c, 0):.2f} |")
|
||||
lines.append("")
|
||||
lines.append(f"**공통 {len(inter_c)}개 (IDF 합 = {sum(idf_concept.get(c, 0) for c in inter_c):.2f})**")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"**MDX03-1 ∩ Frame 15 (오답)**")
|
||||
lines.append("")
|
||||
if inter_w:
|
||||
lines.append(", ".join(sorted(inter_w)))
|
||||
else:
|
||||
lines.append("**공통 개념 0개** — \"요건\" 같은 표면 단어는 TF-IDF 단계에서 걸리지만, AI는 \"요건\"이 Frame 15에서는 \"정책 실행요건\"을 의미하기에 MDX03-1의 \"필수 요건\" 개념과 다르게 추출함")
|
||||
lines.append("")
|
||||
|
||||
lines.append("### AI-Meta가 성공하는 이유")
|
||||
lines.append("")
|
||||
lines.append("1. **동의어 정규화**: AI가 MDX03-1 본문을 읽고 \"필수 요건\"을 직접 단어로 쓰지 않고, 그 내용인 `[기술, 사람, 자연, 여건, ...]`로 개념화. Frame 13도 같은 핵심 개념으로 태그. **표면 단어가 다르더라도 의미 일치**")
|
||||
lines.append("2. **구조 힌트 포함**: `layout_hint: 3col-parallel`이라는 구조 태그가 있어 \"3열 병렬 카드\"끼리 매칭 선호 가능 (현재 구현은 concepts만 쓰지만 layout_hint도 활용 가능)")
|
||||
lines.append("3. **희귀 개념 우대**: IDF로 \"여건, 사람, 자연, 역량\" 같은 이 MDX에만 특징적인 개념은 3.47이라는 큰 가중치. \"디지털\"처럼 많은 프레임에 나오는 일반어는 1.86으로 낮은 가중치")
|
||||
lines.append("4. **근거가 투명**: 어떤 개념이 겹쳐서 몇 점이 됐는지 한눈에 확인 가능. Cross-encoder 같은 블랙박스와 달리 사람이 metadata_db를 직접 편집해서 튜닝 가능")
|
||||
lines.append("")
|
||||
|
||||
# ═══ 요약 비교 ═══
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("# 최종 Before/After 비교")
|
||||
lines.append("")
|
||||
lines.append("| | TF-IDF (키워드만) | AI-Meta (키워드+구조+의미) |")
|
||||
lines.append("|---|---|---|")
|
||||
lines.append("| 1위 선택 | Frame 15 ❌ | Frame 13 ⭐ |")
|
||||
lines.append("| 기준 | 단어 표면 겹침 | 개념 집합 일치 (구조 포함) |")
|
||||
lines.append("| 동의어 | 못 다룸 (\"요건\"↔\"조건\" 실패) | AI가 사전에 같은 개념으로 정규화 |")
|
||||
lines.append("| 구조 | 무시 | layout_hint로 명시적 태깅 |")
|
||||
lines.append("| 설명 가능성 | 공통 단어 나열 → 설명 제한적 | 개념 교집합 + IDF → 명확 |")
|
||||
lines.append("| 튜닝 | corpus 변경하면 IDF 전체 재계산 | metadata_db에 개념 추가/수정으로 조정 |")
|
||||
lines.append("| 비용 | AI 불필요, 가벼움 | 사전 AI 1회 호출 필요 (이후 저장된 데이터만 사용) |")
|
||||
lines.append("")
|
||||
lines.append("**결론**: 키워드 기반은 표면 단어 한계에 막힌다. "
|
||||
"AI가 구조·의미를 미리 개념 집합으로 정규화하는 순간, "
|
||||
"동의어·구조 불일치 문제가 자연스럽게 해결된다.")
|
||||
lines.append("")
|
||||
|
||||
out_path = Path(__file__).parent / "MATRIX_PHASE15.md"
|
||||
out_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
print(f"완료: {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user