Files
C.E.L_Slide_test2/tests/matching/phase13.py
T
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

425 lines
18 KiB
Python
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.
"""Phase 13 — 방법별 매칭 키워드/청크/구조 진단 (유저 요청 포맷)
각 MDX 유닛에 대해 7개 방법 × 상위 3개 후보에 대해 "왜 이 결과가 나왔는가"를 상세히.
- TF-IDF / Kiwi+BM25 / Distinctive-Kiwi: 공유 키워드 + IDF/BM25 기여도
- Char 3-gram: 공유 3-gram 조각
- 구조 메타: 구조 피처 벡터 (행 수, 열 수, 블릿 수, 깊이, 이미지 수)
- 청킹+SBERT / 청킹+E5: 가장 유사한 MDX↔Figma 청크 쌍
- Cross: 단어 수준 설명 불가 → 원점수만
"""
import sys
import json
import math
import collections
import re
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from common import load_figma_texts, tokenize_simple, char_ngrams
from extract_units import extract_units
from methods import (
_get_kiwi, _extract_content_tokens, _chunk_text, _np_cosine,
_get_sbert, _get_e5, _get_cross_encoder,
method_tfidf, method_char_ngram, method_kiwi_bm25,
method_structural, method_sbert_chunk, method_e5_chunk,
method_cross_encoder, method_distinctive_kiwi,
_build_tfidf,
)
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
PREVIEW_DIR = ROOT / "data" / "figma_previews"
TARGET_UNITS = [
("MDX01-2-details", "1. (MDX 1) 팝업 — DX와 BIM의 구분", "18"),
("MDX02-2.2-table", "2. (MDX 2) 2.2 DX 시행 주체별 기대효과", "14"),
("MDX03-1", "3. (MDX 03) 1. DX 시행을 위한 필수요건", "13"),
("MDX03-2", "4. (MDX 03) 2. Process 혁신과 Product 변화", "29"),
]
def short_id(fid, frame_to_short):
return frame_to_short.get(str(fid), "?")
# ═══════════════════════════════════════
# 방법별 "왜 이 후보가 선택되었나" 진단기
# ═══════════════════════════════════════
def diagnose_tfidf(mdx_text, figma_texts, target_fids):
"""TF-IDF: 공유 단어 × IDF 기여도 상위"""
all_docs = {"__mdx__": mdx_text, **figma_texts}
vecs, df, N = _build_tfidf(all_docs)
mdx_vec = vecs["__mdx__"]
result = {}
for fid in target_fids:
figma_vec = vecs.get(fid, {})
common = []
for w, mdx_w in mdx_vec.items():
if w in figma_vec:
contrib = mdx_w * figma_vec[w]
common.append((w, mdx_w, figma_vec[w], contrib))
common.sort(key=lambda x: -x[3])
result[fid] = common[:10]
return result
def diagnose_char_ngram(mdx_text, figma_texts, target_fids, n=3):
"""Char 3-gram: 공유 n-gram 조각"""
mdx_set = set(char_ngrams(mdx_text, n))
result = {}
for fid in target_fids:
figma_set = set(char_ngrams(figma_texts.get(fid, ""), n))
common = mdx_set & figma_set
# 몇개 자주 나오는지 표시
result[fid] = sorted(common)[:20]
return result
def diagnose_bm25(mdx_text, figma_texts, target_fids):
"""Kiwi+BM25: Kiwi 형태소 공유 토큰 + TF-IDF 기여도 (BM25 정확 내부는 복잡하므로 TF-IDF 근사)"""
kiwi = _get_kiwi()
mdx_tokens = _extract_content_tokens(mdx_text, kiwi)
mdx_tf = collections.Counter(mdx_tokens)
# Corpus IDF
doc_tokens = {fid: _extract_content_tokens(text, kiwi) for fid, text in figma_texts.items()}
df = collections.Counter()
for toks in doc_tokens.values():
for w in set(toks):
df[w] += 1
N = len(figma_texts)
idf = {w: math.log(N / c) if c > 0 else 0 for w, c in df.items()}
result = {}
for fid in target_fids:
figma_tf = collections.Counter(doc_tokens.get(fid, []))
common = []
for w, m_c in mdx_tf.items():
if w in figma_tf and idf.get(w, 0) > 0:
contrib = m_c * figma_tf[w] * idf[w]
common.append((w, m_c, figma_tf[w], idf[w], contrib))
common.sort(key=lambda x: -x[4])
result[fid] = common[:10]
return result
def diagnose_distinctive(mdx_text, figma_texts, target_fids, general_threshold=0.5):
"""Distinctive-Kiwi: 공통어 제거 후 남은 키워드"""
kiwi = _get_kiwi()
doc_tokens = {fid: _extract_content_tokens(text, kiwi) for fid, text in figma_texts.items()}
df = collections.Counter()
for toks in doc_tokens.values():
for w in set(toks):
df[w] += 1
N = len(figma_texts)
general = {w for w, c in df.items() if c / N > general_threshold}
idf = {w: math.log(N / c) if c > 0 else 0 for w, c in df.items()}
mdx_tokens = [w for w in _extract_content_tokens(mdx_text, kiwi) if w not in general]
mdx_tf = collections.Counter(mdx_tokens)
result = {}
for fid in target_fids:
figma_tf = collections.Counter(
w for w in doc_tokens.get(fid, []) if w not in general
)
common = []
for w, m_c in mdx_tf.items():
if w in figma_tf:
contrib = m_c * figma_tf[w] * idf.get(w, 0)
common.append((w, m_c, figma_tf[w], idf.get(w, 0), contrib))
common.sort(key=lambda x: -x[4])
result[fid] = common[:10]
return result, general
def diagnose_structural(mdx_text, figma_texts, target_fids):
"""구조 메타: 피처 값 비교"""
def extract_features(text):
return {
"블릿 수": len(re.findall(r"^\s*[-*]\s", text, re.MULTILINE)),
"표 행 수": text.count("\n|"),
"헤더 수 (##)": text.count("\n##"),
"이미지 수": len(re.findall(r"!\[[^\]]*\]\([^)]+\)", text)),
"텍스트 길이": len(text),
}
mdx_feat = extract_features(mdx_text)
result = {}
for fid in target_fids:
figma_feat = extract_features(figma_texts.get(fid, ""))
result[fid] = {"mdx": mdx_feat, "figma": figma_feat}
return result, mdx_feat
def diagnose_sbert_chunk(mdx_text, figma_texts, target_fids):
"""청킹+SBERT: MDX↔Figma 가장 유사한 청크 쌍 top 3"""
model = _get_sbert()
q_chunks = _chunk_text(mdx_text)
q_vecs = model.encode(q_chunks, show_progress_bar=False)
result = {}
for fid in target_fids:
d_chunks = _chunk_text(figma_texts.get(fid, ""))
if not d_chunks:
result[fid] = []
continue
d_vecs = model.encode(d_chunks, show_progress_bar=False)
pairs = []
for i, qv in enumerate(q_vecs):
for j, dv in enumerate(d_vecs):
sim = _np_cosine(qv, dv)
pairs.append((sim, q_chunks[i], d_chunks[j]))
pairs.sort(key=lambda x: -x[0])
result[fid] = pairs[:3]
return result
def diagnose_e5_chunk(mdx_text, figma_texts, target_fids):
"""청킹+E5"""
model = _get_e5()
q_chunks = _chunk_text(mdx_text)
q_vecs = model.encode(["query: " + c for c in q_chunks],
show_progress_bar=False, normalize_embeddings=True)
result = {}
for fid in target_fids:
d_chunks = _chunk_text(figma_texts.get(fid, ""))
if not d_chunks:
result[fid] = []
continue
d_vecs = model.encode(["passage: " + c for c in d_chunks],
show_progress_bar=False, normalize_embeddings=True)
pairs = []
for i, qv in enumerate(q_vecs):
for j, dv in enumerate(d_vecs):
sim = _np_cosine(qv, dv)
pairs.append((sim, q_chunks[i], d_chunks[j]))
pairs.sort(key=lambda x: -x[0])
result[fid] = pairs[:3]
return result
# ═══════════════════════════════════════
# 메인
# ═══════════════════════════════════════
def trunc(s, n=45):
s = s.replace("\n", " ").strip()
return s if len(s) <= n else s[:n] + "…"
def main():
units = extract_units()
figma = load_figma_texts()
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/"
lines = []
lines.append("# Phase 13 — 방법별 매칭 키워드/청크/구조 진단")
lines.append("")
lines.append("각 MDX 유닛 × 7개 방법 × Top-3 후보에 대해 "
"**왜 이 결과가 나왔는지**를 매칭 키워드/청크/구조 단위로 보여줍니다.")
lines.append("")
for uid, display, correct_id in TARGET_UNITS:
lines.append("---")
lines.append("")
lines.append(f"# {display}")
lines.append(f"**정답**: Frame **{correct_id}**")
lines.append("")
mdx_text = units[uid]
# 각 방법 Top-3 얻기
results_by_method = {}
results_by_method["TF-IDF"] = method_tfidf(mdx_text, figma)[:3]
results_by_method["Char 3-gram"] = method_char_ngram(mdx_text, figma)[:3]
results_by_method["Kiwi+BM25"] = method_kiwi_bm25(mdx_text, figma)[:3]
results_by_method["Distinctive-Kiwi"] = method_distinctive_kiwi(mdx_text, figma)[:3]
results_by_method["구조 메타"] = method_structural(mdx_text, figma)[:3]
results_by_method["청킹+SBERT"] = method_sbert_chunk(mdx_text, figma)[:3]
results_by_method["청킹+E5"] = method_e5_chunk(mdx_text, figma)[:3]
results_by_method["Cross"] = method_cross_encoder(mdx_text, figma)[:3]
# 각 방법별 Target IDs (Top-3)
all_target_ids = set()
for method, ranked in results_by_method.items():
for fid, _ in ranked:
all_target_ids.add(fid)
# 진단 계산 (한 번씩)
tfidf_diag = diagnose_tfidf(mdx_text, figma, all_target_ids)
charng_diag = diagnose_char_ngram(mdx_text, figma, all_target_ids)
bm25_diag = diagnose_bm25(mdx_text, figma, all_target_ids)
distinct_diag, general_words = diagnose_distinctive(mdx_text, figma, all_target_ids)
struct_diag, mdx_feat = diagnose_structural(mdx_text, figma, all_target_ids)
sbert_diag = diagnose_sbert_chunk(mdx_text, figma, all_target_ids)
e5_diag = diagnose_e5_chunk(mdx_text, figma, all_target_ids)
def frame_preview_cell(fid, score_str):
sid = frame_to_short.get(str(fid), "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
title = info.get("title_text", "").strip().replace("\n", " ") or ""
if len(title) > 15:
title = title[:15] + "…"
mark = "⭐ " if sid == correct_id else ""
return f"{mark}![{sid}]({png_rel}{png})<br>**{sid}** ({score_str})<br>{title}"
# 방법별 섹션
# 1. TF-IDF
lines.append("## 키워드 매칭 — TF-IDF")
lines.append("")
lines.append("각 후보 Figma와의 **공유 단어(IDF 기여도 순 상위 10개)**")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["TF-IDF"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = tfidf_diag.get(fid, [])
if common:
lines.append("| 단어 | MDX TF-IDF | Figma TF-IDF | 기여도 |")
lines.append("|------|------------|--------------|--------|")
for w, mdx_w, figma_w, contrib in common:
lines.append(f"| {w} | {mdx_w:.3f} | {figma_w:.3f} | {contrib:.4f} |")
else:
lines.append("공유 단어 없음")
lines.append("")
# 2. Char 3-gram
lines.append("## 키워드 매칭 — Char 3-gram")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Char 3-gram"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = charng_diag.get(fid, [])
if common:
sample = ", ".join(f"`{s}`" for s in common[:20])
lines.append(f"**공유된 3-gram 조각 (상위 20개)**: {sample}")
else:
lines.append("공유 3-gram 없음")
lines.append("")
# 3. Kiwi+BM25
lines.append("## 키워드 매칭 — Kiwi+BM25")
lines.append("")
lines.append("Kiwi 형태소 추출 후 **공유 형태소 × IDF 기여도 상위 10개**")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Kiwi+BM25"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — BM25 점수 {score:.1f}")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = bm25_diag.get(fid, [])
if common:
lines.append("| 형태소 | MDX TF | Figma TF | IDF | 기여도 |")
lines.append("|-------|--------|----------|-----|--------|")
for w, mc, fc, widf, contrib in common:
lines.append(f"| {w} | {mc} | {fc} | {widf:.2f} | {contrib:.2f} |")
else:
lines.append("공유 형태소 없음")
lines.append("")
# 4. Distinctive-Kiwi
lines.append("## 키워드 매칭 — Distinctive-Kiwi")
lines.append("")
lines.append(f"**공통어로 제거된 단어**: {', '.join(sorted(general_words))}")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Distinctive-Kiwi"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = distinct_diag.get(fid, [])
if common:
lines.append("| 고유 형태소 | MDX TF | Figma TF | IDF | 기여도 |")
lines.append("|----------|--------|----------|-----|--------|")
for w, mc, fc, widf, contrib in common:
lines.append(f"| {w} | {mc} | {fc} | {widf:.2f} | {contrib:.2f} |")
else:
lines.append("공유 고유 형태소 없음")
lines.append("")
# 5. 구조 메타
lines.append("## 구조 매칭 — 구조 메타")
lines.append("")
lines.append(f"**MDX 구조 피처**: {mdx_feat}")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["구조 메타"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 유사도 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
feat = struct_diag.get(fid, {}).get("figma", {})
lines.append("| 피처 | MDX | Figma |")
lines.append("|------|-----|-------|")
for key in mdx_feat:
lines.append(f"| {key} | {mdx_feat[key]} | {feat.get(key, 0)} |")
lines.append("")
# 6. 청킹+SBERT
lines.append("## 의미 매칭 — 청킹+SBERT")
lines.append("")
lines.append("MDX와 Figma 각각 블릿/섹션 단위로 쪼갠 뒤 **가장 유사한 청크 쌍 Top 3**")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["청킹+SBERT"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 평균 유사도 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
pairs = sbert_diag.get(fid, [])
if pairs:
lines.append("| 유사도 | MDX 청크 | Figma 청크 |")
lines.append("|--------|---------|------------|")
for sim, mc, fc in pairs:
lines.append(f"| {sim * 100:.0f}% | {trunc(mc, 40)} | {trunc(fc, 40)} |")
lines.append("")
# 7. 청킹+E5
lines.append("## 의미 매칭 — 청킹+E5")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["청킹+E5"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 평균 유사도 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
pairs = e5_diag.get(fid, [])
if pairs:
lines.append("| 유사도 | MDX 청크 | Figma 청크 |")
lines.append("|--------|---------|------------|")
for sim, mc, fc in pairs:
lines.append(f"| {sim * 100:.0f}% | {trunc(mc, 40)} | {trunc(fc, 40)} |")
lines.append("")
# 8. Cross
lines.append("## 의미 매칭 — Cross")
lines.append("")
lines.append("*Cross-encoder는 질의-문서 쌍을 통째로 채점해서 단어/청크 수준 설명 불가. 원 점수만 표시.*")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Cross"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = " ⭐" if sid == correct_id else ""
lines.append(f"- {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.1f}%")
lines.append("")
out_path = Path(__file__).parent / "MATRIX_PHASE13.md"
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"완료: {out_path}")
if __name__ == "__main__":
main()