"""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})
**{sid}** ({score_str})
{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()