File size: 6,696 Bytes
23e73f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import os
import glob
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from tensorboard.backend.event_processing import event_accumulator

# === 论文级图表样式设置 ===
sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    'font.size': 12,
    'axes.titlesize': 14,
    'axes.labelsize': 12,
    'figure.autolayout': True,
    'figure.dpi': 300
})

def extract_tb_data(log_dir):
    """从 TensorBoard 目录中提取所有 Scalar 数据为 Pandas DataFrame"""
    event_files = glob.glob(os.path.join(log_dir, 'events.out.tfevents.*'))
    if not event_files:
        return None
    
    # size_guidance=0 保证我们不会截断任何一个 step 的数据
    ea = event_accumulator.EventAccumulator(log_dir, size_guidance={event_accumulator.SCALARS: 0})
    ea.Reload()
    
    extracted_data = {}
    if 'scalars' not in ea.Tags():
        return None
        
    for tag in ea.Tags()['scalars']:
        events = ea.Scalars(tag)
        # 将每个指标转换为 DataFrame
        extracted_data[tag] = pd.DataFrame(
            [(e.step, e.value) for e in events], 
            columns=['step', tag]
        ).set_index('step')
        
    if not extracted_data:
        return None
        
    # 以 step 为基准,横向拼接所有监控指标
    df = pd.concat(extracted_data.values(), axis=1).reset_index()
    return df

def main():
    outputs_dir = os.path.join(os.path.dirname(__file__), "..", "outputs")
    plots_dir = os.path.join(os.path.dirname(__file__), "..", "plots")
    os.makedirs(plots_dir, exist_ok=True)
    
    all_data = []
    
    print("🔍 [Harvester] 正在扫描 TensorBoard 日志...")
    for folder in os.listdir(outputs_dir):
        log_dir = os.path.join(outputs_dir, folder)
        # 忽略非目录和病理学隔离区(可选,这里为了看全盘先全扫)
        if not os.path.isdir(log_dir) or folder == "pathology_archive":
            continue
            
        df = extract_tb_data(log_dir)
        if df is not None:
            # 假设文件夹命名规范为 method_scene
            parts = folder.split('_', 1)
            method = parts[0] if len(parts) > 1 else folder
            
            df['method'] = method
            df['experiment'] = folder
            all_data.append(df)
            print(f"  ✅ 成功提取: {folder} ({len(df)} 条时间序列记录)")
            
    if not all_data:
        print("⚠️ 未找到任何有效的 TensorBoard 数据!")
        return
        
    master_df = pd.concat(all_data, ignore_index=True)
    
    # 动态匹配我们埋下的探针列名
    time_col = 'hardware/cumulative_time_sec' if 'hardware/cumulative_time_sec' in master_df.columns else 'step'
    n_col = 'geometry/N_gaussians'
    loss_col = 'train/loss'
    cos_col = 'dynamics_decoupled/cos_similarity'
    gamma_col = 'pathology/gamma_median'
    
    print(f"\n📊 [Harvester] 正在使用 Matplotlib 绘制动力学大图,目标目录: {plots_dir}/")
    
    # =========================================================
    # 图表 1: 边际收益崩塌与致密化 S 曲线 (The Marginal Return Curve)
    # =========================================================
    if n_col in master_df.columns and loss_col in master_df.columns:
        plt.figure(figsize=(10, 6))
        ax1 = plt.gca()
        ax2 = ax1.twinx()
        
        colors = sns.color_palette("husl", len(master_df['experiment'].unique()))
        
        for idx, exp in enumerate(master_df['experiment'].unique()):
            exp_df = master_df[master_df['experiment'] == exp].dropna(subset=[time_col, n_col, loss_col])
            if exp_df.empty: continue
            
            color = colors[idx]
            # 绘制 N 数量的 S 曲线 (实线)
            ax1.plot(exp_df[time_col], exp_df[n_col], linestyle='-', linewidth=2.5, color=color, label=f"{exp} (N)")
            # 绘制 Loss 下降曲线 (虚线)
            ax2.plot(exp_df[time_col], exp_df[loss_col], linestyle='--', linewidth=2, color=color, alpha=0.6)
            
        xlabel = 'Cumulative GPU Time (Seconds)' if time_col != 'step' else 'Training Steps'
        ax1.set_xlabel(xlabel, fontweight='bold')
        ax1.set_ylabel('Number of Gaussians (N)', color='black', fontweight='bold')
        ax2.set_ylabel('Training Loss (L1 + SSIM)', color='gray', fontweight='bold')
        
        plt.title('Densification Dynamics & Loss Convergence', pad=20, fontweight='bold')
        ax1.legend(loc='upper left', bbox_to_anchor=(1.15, 1))
        
        out_path1 = os.path.join(plots_dir, 'fig1_marginal_return.png')
        plt.savefig(out_path1, bbox_inches='tight')
        plt.close()
        print(f"  📸 论文大图 1 已生成: {out_path1}")

    # =========================================================
    # 图表 2: 光度-几何代偿动力学散点图 (The Compensation Scatter)
    # =========================================================
    if cos_col in master_df.columns and gamma_col in master_df.columns:
        plot_df = master_df.dropna(subset=[cos_col, gamma_col]).copy()
        if not plot_df.empty:
            plt.figure(figsize=(9, 7))
            
            # 使用散点图,横坐标是算子夹角,纵坐标是畸变率,点的大小代表训练进度(Step)
            scatter = sns.scatterplot(
                data=plot_df, 
                x=cos_col, 
                y=gamma_col, 
                hue='method',
                size='step',
                sizes=(20, 200),
                alpha=0.8,
                palette='Set1',
                edgecolor='black'
            )
            
            # 增加物理意义的警戒线
            plt.axvline(x=0, color='gray', linestyle='--', alpha=0.5, label='Orthogonal Gradients (0)')
            plt.axhline(y=18, color='red', linestyle=':', linewidth=2, alpha=0.7, label=r'Pathology Threshold ($\gamma=18$)')
            
            plt.xlabel(r'Gradient Cosine Similarity $\cos(\theta)$ (Target vs Parasitic)', fontweight='bold')
            plt.ylabel(r'Anisotropy Distortion $\gamma$ (Max/Min Scale)', fontweight='bold')
            plt.title('Photometric-Geometric Compensation Phase Space', pad=20, fontweight='bold')
            
            plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
            
            out_path2 = os.path.join(plots_dir, 'fig2_compensation_scatter.png')
            plt.savefig(out_path2, bbox_inches='tight')
            plt.close()
            print(f"  📸 论文大图 2 已生成: {out_path2}")

    print("✅ [Harvester] 您的核心科研图表已就绪!")

if __name__ == "__main__":
    main()