- 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>
122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
"""Pipeline Step 15 (BM25) — 정식 BM25 로 랭킹 계산해서 현재 가중치 방식과 비교.
|
||
|
||
BM25 공식 (표준):
|
||
score(D, Q) = Σ_{t∈Q ∩ D} IDF(t) × (tf(t,D) × (k1+1)) / (tf(t,D) + k1 × (1 - b + b × |D|/avg_dl))
|
||
IDF(t) = log((N - n(t) + 0.5) / (n(t) + 0.5) + 1)
|
||
|
||
설정:
|
||
D = 각 Figma 프레임 (키워드 멀티셋)
|
||
Q = MDX 섹션 토큰 집합
|
||
N = 32 프레임
|
||
n(t) = t 를 포함한 프레임 수 (frame_df)
|
||
tf(t, D) = 프레임 D 에서 t 의 local_count
|
||
|D| = 프레임 D 의 총 토큰 수
|
||
avg_dl = 모든 프레임 평균 길이
|
||
k1 = 1.5, b = 0.75 (표준값)
|
||
|
||
비교: 현재 0.30/0.50/0.20 가중치 결과 vs BM25 결과.
|
||
"""
|
||
from collections import defaultdict
|
||
from math import log
|
||
from pathlib import Path
|
||
import yaml
|
||
|
||
HERE = Path(__file__).parent
|
||
N_FRAMES = 32
|
||
K1 = 1.5
|
||
B = 0.75
|
||
|
||
|
||
def main():
|
||
auto = yaml.safe_load((HERE / 'auto_anchor_candidates.yaml').read_text(encoding='utf-8'))
|
||
v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8'))
|
||
normalized = yaml.safe_load((HERE / 'normalized_text_tokens.yaml').read_text(encoding='utf-8'))
|
||
|
||
# ─── 프레임별 토큰 multiset (tf = 등장 횟수) ───
|
||
frame_tokens = defaultdict(lambda: defaultdict(int))
|
||
for set_id, s in auto['source_text_sets'].items():
|
||
fid = s['frame_id']
|
||
for t in s.get('terms', []):
|
||
tok = t['token']
|
||
frame_tokens[fid][tok] += t.get('local_count_in_frame', 1)
|
||
|
||
# 프레임 길이 |D| (= 모든 토큰 local_count 합)
|
||
frame_len = {fid: sum(c.values()) for fid, c in frame_tokens.items()}
|
||
avg_dl = sum(frame_len.values()) / max(len(frame_len), 1)
|
||
|
||
# n(t): t 를 포함하는 프레임 수
|
||
frame_df_map = defaultdict(int)
|
||
for fid, counts in frame_tokens.items():
|
||
for tok in counts:
|
||
frame_df_map[tok] += 1
|
||
|
||
def idf_bm25(t):
|
||
n = frame_df_map.get(t, 0)
|
||
return log((N_FRAMES - n + 0.5) / (n + 0.5) + 1)
|
||
|
||
def bm25_score(frame_id, mdx_tokens):
|
||
counts = frame_tokens[frame_id]
|
||
dl = frame_len[frame_id]
|
||
if dl == 0:
|
||
return 0.0
|
||
score = 0.0
|
||
for t in mdx_tokens:
|
||
if t in counts:
|
||
tf = counts[t]
|
||
idf_val = idf_bm25(t)
|
||
denom = tf + K1 * (1 - B + B * dl / avg_dl)
|
||
tf_norm = tf * (K1 + 1) / denom
|
||
score += idf_val * tf_norm
|
||
return score
|
||
|
||
# frame_id → frame_number
|
||
frame_num_map = {fid: info['frame_number'] for fid, info in auto['frame_stats'].items()}
|
||
|
||
# ─── 각 MDX 섹션별 계산 ───
|
||
print('=' * 90)
|
||
print(f'{"섹션":<8} | {"순위":<3} | {"현재 (0.30/0.50/0.20)":<28} | {"BM25 (k1=1.5, b=0.75)":<28}')
|
||
print('=' * 90)
|
||
|
||
top1_agree = 0
|
||
top3_agree = 0
|
||
total = 0
|
||
|
||
for sid, sec in v1['mdx_sections'].items():
|
||
mdx_tokens = set(normalized['mdx'][sid].get('unique_tokens', []))
|
||
bm25_scores = {
|
||
fid: (bm25_score(fid, mdx_tokens), frame_num_map.get(fid))
|
||
for fid in frame_tokens
|
||
}
|
||
bm25_ranking = sorted(bm25_scores.items(), key=lambda x: -x[1][0])
|
||
|
||
current_ranking = sec['rank_by_matching_score']
|
||
total += 1
|
||
if current_ranking[0]['frame_id'] == bm25_ranking[0][0]:
|
||
top1_agree += 1
|
||
cur_top3 = {r['frame_id'] for r in current_ranking[:3]}
|
||
bm25_top3 = {x[0] for x in bm25_ranking[:3]}
|
||
if cur_top3 == bm25_top3:
|
||
top3_agree += 1
|
||
|
||
for i in range(5):
|
||
cur = current_ranking[i]
|
||
bm = bm25_ranking[i]
|
||
cur_s = f'Frame {cur["frame_number"]:>2} score {cur["matching_score"]:.3f}'
|
||
bm_s = f'Frame {bm[1][1]:>2} score {bm[1][0]:>6.2f}'
|
||
prefix = sid if i == 0 else ''
|
||
print(f'{prefix:<8} | {i+1:<3} | {cur_s:<28} | {bm_s:<28}')
|
||
print('-' * 90)
|
||
|
||
print()
|
||
print('=' * 90)
|
||
print(f'Top-1 일치: {top1_agree}/{total}')
|
||
print(f'Top-3 집합 일치: {top3_agree}/{total}')
|
||
print('=' * 90)
|
||
print()
|
||
print(f'N = {N_FRAMES} 프레임, avg_dl = {avg_dl:.1f} (평균 프레임 토큰 수)')
|
||
print(f'k1 = {K1}, b = {B} (표준 BM25 파라미터)')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|