172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Bar plot comparing CNN standalone vs HyperTower image_only AUC per backbone,
|
|
with PAPILA paper reference lines.
|
|
|
|
Usage:
|
|
python -m v3.scripts.output_analysis.plot_cnn_backbone_comparison \
|
|
--results-dir v3/results/phase1 \
|
|
--output v3/results/phase1/cnn_backbone_comparison.png
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
import pandas as pd
|
|
from sklearn.metrics import roc_auc_score
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
|
|
|
|
|
|
BACKBONES = ["densenet121", "vgg16", "mobilenet_v2", "inception_v3", "resnet50"]
|
|
BACKBONE_LABELS = {
|
|
"densenet121": "DenseNet121",
|
|
"vgg16": "VGG16",
|
|
"mobilenet_v2": "MobileNetV2",
|
|
"inception_v3": "Inception V3",
|
|
"resnet50": "ResNet50",
|
|
}
|
|
|
|
# Per-backbone paper AUCs (binary, Test #2, PAPILA 2022)
|
|
PAPER_AUC = {
|
|
"densenet121": 0.80,
|
|
"vgg16": 0.84,
|
|
"mobilenet_v2": 0.75,
|
|
"inception_v3": 0.78,
|
|
"resnet50": 0.78,
|
|
}
|
|
PAPER_STD = {
|
|
"densenet121": 0.05,
|
|
"vgg16": 0.02,
|
|
"mobilenet_v2": 0.06,
|
|
"inception_v3": 0.08,
|
|
"resnet50": 0.07,
|
|
}
|
|
|
|
COLOURS = {
|
|
"cnn": "#4878CF",
|
|
"ht": "#D65F5F",
|
|
"paper_ref": "black",
|
|
}
|
|
|
|
|
|
def load_cnn_fold_aucs(results_dir: Path, backbone: str) -> list[float]:
|
|
fpath = results_dir / f"cnn_{backbone}" / "fold_metrics.csv"
|
|
if not fpath.exists():
|
|
print(f" WARNING: missing {fpath}")
|
|
return []
|
|
df = pd.read_csv(fpath)
|
|
return df["auc"].tolist()
|
|
|
|
|
|
def load_ht_fold_aucs(results_dir: Path, backbone: str, n_folds: int = 5) -> list[float]:
|
|
aucs = []
|
|
for fold in range(n_folds):
|
|
fold_dir = results_dir / "imageonly_ht" / backbone / "binary" / "single" / f"fold{fold}"
|
|
y_path = fold_dir / "test_y_true.npy"
|
|
p_path = fold_dir / "test_probs_fused.npy"
|
|
if not (y_path.exists() and p_path.exists()):
|
|
print(f" WARNING: missing predictions for {backbone} fold{fold}")
|
|
continue
|
|
y = np.load(y_path)
|
|
pr = np.load(p_path)
|
|
if len(np.unique(y)) < 2:
|
|
print(f" WARNING: single-class test set for {backbone} fold{fold}, skipping")
|
|
continue
|
|
aucs.append(float(roc_auc_score(y, pr[:, 1])))
|
|
return aucs
|
|
|
|
|
|
def plot(cnn_data: dict, ht_data: dict, output: Path):
|
|
n = len(BACKBONES)
|
|
x = np.arange(n)
|
|
group_width = 0.7
|
|
bar_w = group_width / 2 * 0.88
|
|
offsets = [-group_width / 4, group_width / 4]
|
|
|
|
fig, ax = plt.subplots(figsize=(10, 5.5))
|
|
|
|
for bi, backbone in enumerate(BACKBONES):
|
|
for si, (tag, data, colour) in enumerate([
|
|
("CNN standalone", cnn_data, COLOURS["cnn"]),
|
|
("HyperTower (image only)", ht_data, COLOURS["ht"]),
|
|
]):
|
|
aucs = data.get(backbone, [])
|
|
if not aucs:
|
|
continue
|
|
xpos = bi + offsets[si]
|
|
mean, std = np.mean(aucs), np.std(aucs)
|
|
ax.bar(
|
|
xpos, mean, width=bar_w,
|
|
color=colour, alpha=0.80,
|
|
label=tag if bi == 0 else "_nolegend_",
|
|
)
|
|
ax.errorbar(
|
|
xpos, mean, yerr=std,
|
|
fmt="none", color="black", capsize=4, linewidth=1.2,
|
|
)
|
|
|
|
# Paper reference line spanning this backbone's group
|
|
paper_val = PAPER_AUC.get(backbone)
|
|
if paper_val is not None:
|
|
lw = group_width / 2 + bar_w / 2
|
|
label = "PAPILA paper" if bi == 0 else "_nolegend_"
|
|
ax.hlines(
|
|
paper_val,
|
|
bi - group_width / 2, bi + group_width / 2,
|
|
colors=COLOURS["paper_ref"], linestyles=":", linewidths=1.8,
|
|
label=label,
|
|
)
|
|
|
|
ax.set_xticks(x)
|
|
ax.set_xticklabels([BACKBONE_LABELS[b] for b in BACKBONES], fontsize=11)
|
|
ax.set_ylabel("AUC (ROC)", fontsize=11)
|
|
ax.set_title("Phase 1: CNN backbone AUC — standalone vs HyperTower (image only)", fontsize=12)
|
|
ax.set_ylim(0.45, 1.02)
|
|
ax.axhline(0.5, color="grey", linestyle="--", linewidth=0.8, alpha=0.4)
|
|
ax.grid(axis="y", alpha=0.3, linestyle="--")
|
|
ax.legend(loc="lower right", fontsize=10, framealpha=0.9)
|
|
fig.tight_layout()
|
|
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
fig.savefig(output, dpi=180)
|
|
plt.close(fig)
|
|
print(f"Saved: {output}")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--results-dir", default="v3/results/phase1")
|
|
ap.add_argument("--output", default=None)
|
|
args = ap.parse_args()
|
|
|
|
results_dir = Path(args.results_dir)
|
|
output = Path(args.output) if args.output else results_dir / "cnn_backbone_comparison.png"
|
|
|
|
cnn_data = {b: load_cnn_fold_aucs(results_dir, b) for b in BACKBONES}
|
|
ht_data = {b: load_ht_fold_aucs(results_dir, b) for b in BACKBONES}
|
|
|
|
plot(cnn_data, ht_data, output)
|
|
|
|
# Summary table
|
|
print(f"\n{'Backbone':<16} {'CNN standalone':>18} {'HT image_only':>18} {'Paper':>12}")
|
|
print("-" * 72)
|
|
for b in BACKBONES:
|
|
cnn_aucs = cnn_data[b]
|
|
ht_aucs = ht_data[b]
|
|
cnn_str = f"{np.mean(cnn_aucs):.3f} ± {np.std(cnn_aucs):.3f}" if cnn_aucs else "—"
|
|
ht_str = f"{np.mean(ht_aucs):.3f} ± {np.std(ht_aucs):.3f}" if ht_aucs else "—"
|
|
p_str = f"{PAPER_AUC[b]:.2f} ± {PAPER_STD[b]:.2f}"
|
|
print(f"{BACKBONE_LABELS[b]:<16} {cnn_str:>18} {ht_str:>18} {p_str:>12}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|