| |
| """Compare loss curves between LoRA and full fine-tuning runs. |
| |
| Usage: |
| python scripts/compare_loss.py \ |
| --lora-log ./checkpoints/dreamzero_droid_lora/loss_log.jsonl \ |
| --full-log ./checkpoints/dreamzero_droid_full_finetune/loss_log.jsonl \ |
| [--plot loss_comparison.png] |
| """ |
|
|
| import argparse |
| import json |
|
|
|
|
| def load_loss_log(path): |
| entries = [] |
| with open(path) as f: |
| for line in f: |
| line = line.strip() |
| if line: |
| entries.append(json.loads(line)) |
| return entries |
|
|
|
|
| def print_comparison_table(lora_entries, full_entries): |
| |
| lora_by_step = {e["step"]: e for e in lora_entries} |
| full_by_step = {e["step"]: e for e in full_entries} |
| all_steps = sorted(set(lora_by_step.keys()) | set(full_by_step.keys())) |
|
|
| header = f"{'Step':>6} {'LoRA Loss':>10} {'Full Loss':>10} {'LoRA Dyn':>10} {'Full Dyn':>10} {'LoRA Act':>10} {'Full Act':>10}" |
| print(header) |
| print("-" * len(header)) |
|
|
| for step in all_steps: |
| lora = lora_by_step.get(step, {}) |
| full = full_by_step.get(step, {}) |
|
|
| def fmt(d, key): |
| v = d.get(key) |
| return f"{v:10.4f}" if v is not None else f"{'—':>10}" |
|
|
| print( |
| f"{step:>6} " |
| f"{fmt(lora, 'loss')} {fmt(full, 'loss')} " |
| f"{fmt(lora, 'dynamics_loss_avg')} {fmt(full, 'dynamics_loss_avg')} " |
| f"{fmt(lora, 'action_loss_avg')} {fmt(full, 'action_loss_avg')}" |
| ) |
|
|
|
|
| def plot_comparison(lora_entries, full_entries, output_path): |
| try: |
| import matplotlib.pyplot as plt |
| except ImportError: |
| print("matplotlib not installed, skipping plot generation.") |
| print("Install with: pip install matplotlib") |
| return |
|
|
| metrics = [ |
| ("loss", "Total Loss"), |
| ("dynamics_loss_avg", "Dynamics Loss"), |
| ("action_loss_avg", "Action Loss"), |
| ] |
|
|
| fig, axes = plt.subplots(1, len(metrics), figsize=(5 * len(metrics), 4)) |
| if len(metrics) == 1: |
| axes = [axes] |
|
|
| for ax, (key, title) in zip(axes, metrics): |
| lora_steps = [e["step"] for e in lora_entries if key in e] |
| lora_vals = [e[key] for e in lora_entries if key in e] |
| full_steps = [e["step"] for e in full_entries if key in e] |
| full_vals = [e[key] for e in full_entries if key in e] |
|
|
| if lora_steps: |
| ax.plot(lora_steps, lora_vals, label="LoRA", marker="o", markersize=3) |
| if full_steps: |
| ax.plot(full_steps, full_vals, label="Full FT", marker="s", markersize=3) |
|
|
| ax.set_title(title) |
| ax.set_xlabel("Step") |
| ax.set_ylabel("Loss") |
| ax.legend() |
| ax.grid(True, alpha=0.3) |
|
|
| fig.tight_layout() |
| fig.savefig(output_path, dpi=150) |
| print(f"Plot saved to {output_path}") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Compare LoRA vs full fine-tuning loss curves") |
| parser.add_argument("--lora-log", required=True, help="Path to LoRA run loss_log.jsonl") |
| parser.add_argument("--full-log", required=True, help="Path to full FT run loss_log.jsonl") |
| parser.add_argument("--plot", default=None, help="Output path for comparison plot (e.g., loss_comparison.png)") |
| args = parser.parse_args() |
|
|
| lora_entries = load_loss_log(args.lora_log) |
| full_entries = load_loss_log(args.full_log) |
|
|
| print(f"LoRA: {len(lora_entries)} log entries") |
| print(f"Full: {len(full_entries)} log entries") |
| print() |
|
|
| print_comparison_table(lora_entries, full_entries) |
|
|
| if args.plot: |
| plot_comparison(lora_entries, full_entries, args.plot) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|