Rthur2003 commited on
Commit
917199d
·
1 Parent(s): 255af12

feat: add deep-learning figure generation script for enhanced model evaluation

Browse files
Files changed (1) hide show
  1. app/training/generate_deep_figures.py +431 -0
app/training/generate_deep_figures.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deep-learning style publication figures.
2
+
3
+ Produces additional academic figures beyond the 8 base charts:
4
+ - training_history.png — XGBoost learning curve across boosting rounds
5
+ - per_class_metrics.png — precision/recall/F1 per class (Human/AI)
6
+ - learning_curve.png — train vs CV score vs training set size
7
+ - threshold_sweep.png — precision/recall/F1 across thresholds
8
+ - score_distribution.png — predicted-probability histogram by true class
9
+ - per_source_performance.png — breakdown by dataset source
10
+ - classification_report.png — styled report table
11
+
12
+ Usage:
13
+ python -m app.training.generate_deep_figures
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import csv
19
+ import json
20
+ import pickle
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+ import matplotlib
25
+ matplotlib.use("Agg")
26
+ import matplotlib.pyplot as plt
27
+ import matplotlib.patches as mpatches
28
+
29
+ from sklearn.metrics import (
30
+ precision_recall_fscore_support,
31
+ precision_score, recall_score, f1_score,
32
+ )
33
+ from sklearn.model_selection import (
34
+ StratifiedKFold, cross_val_predict, learning_curve,
35
+ )
36
+ from sklearn.base import clone
37
+
38
+ BACKEND = Path(__file__).resolve().parents[2]
39
+ MODELS_DIR = BACKEND / "models"
40
+ DATASET_DIR = BACKEND.parent / "DataSet"
41
+ FIGURES_DIR = DATASET_DIR / "figures"
42
+ FEATURES_CSV = DATASET_DIR / "features.csv"
43
+ METADATA_CSV = DATASET_DIR / "metadata.csv"
44
+
45
+ PALETTE = {
46
+ "bg": "#faf6ed",
47
+ "fg": "#3d2817",
48
+ "primary": "#c99347",
49
+ "secondary": "#7fb069",
50
+ "error": "#a64b3c",
51
+ "grid": "#d8c9a8",
52
+ "accent": "#e7c77a",
53
+ "human": "#7fb069",
54
+ "ai": "#a64b3c",
55
+ }
56
+
57
+ plt.rcParams.update({
58
+ "figure.facecolor": PALETTE["bg"],
59
+ "axes.facecolor": PALETTE["bg"],
60
+ "axes.edgecolor": PALETTE["fg"],
61
+ "axes.labelcolor": PALETTE["fg"],
62
+ "xtick.color": PALETTE["fg"],
63
+ "ytick.color": PALETTE["fg"],
64
+ "text.color": PALETTE["fg"],
65
+ "font.family": "DejaVu Sans",
66
+ "font.size": 11,
67
+ "axes.grid": True,
68
+ "grid.color": PALETTE["grid"],
69
+ "grid.alpha": 0.4,
70
+ "savefig.dpi": 150,
71
+ "savefig.bbox": "tight",
72
+ "figure.dpi": 100,
73
+ })
74
+
75
+
76
+ def _load():
77
+ with open(MODELS_DIR / "auris_classifier_v1.pkl", "rb") as f:
78
+ model = pickle.load(f)
79
+ with open(MODELS_DIR / "feature_scaler_v1.pkl", "rb") as f:
80
+ scaler = pickle.load(f)
81
+ with open(MODELS_DIR / "feature_columns_v1.json", "r") as f:
82
+ feature_cols = json.load(f)
83
+ with open(MODELS_DIR / "training_results.json", "r") as f:
84
+ results = json.load(f)
85
+ return model, scaler, feature_cols, results
86
+
87
+
88
+ def _load_data(feature_cols):
89
+ with open(FEATURES_CSV, "r", encoding="utf-8") as f:
90
+ rows = list(csv.DictReader(f))
91
+ X = np.array([[float(r[c]) for c in feature_cols] for r in rows])
92
+ X = np.nan_to_num(X, nan=0.0, posinf=1.0, neginf=-1.0)
93
+ y = np.array([int(r["label_int"]) for r in rows])
94
+ paths = [r.get("path", "") for r in rows]
95
+ return X, y, paths, rows
96
+
97
+
98
+ def _cv_predict(model, X_scaled, y):
99
+ cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
100
+ y_prob = cross_val_predict(
101
+ clone(model), X_scaled, y, cv=cv, method="predict_proba", n_jobs=-1,
102
+ )[:, 1]
103
+ y_pred = (y_prob > 0.5).astype(int)
104
+ return y_pred, y_prob
105
+
106
+
107
+ # ── 1. Training history (XGBoost boosting-round learning curve) ──────────
108
+ def fig_training_history(model, scaler, X, y):
109
+ """Retrain lightly with eval_set to capture boosting progression."""
110
+ from xgboost import XGBClassifier
111
+ from sklearn.model_selection import train_test_split
112
+
113
+ X_scaled = scaler.transform(X)
114
+ X_tr, X_val, y_tr, y_val = train_test_split(
115
+ X_scaled, y, test_size=0.2, stratify=y, random_state=42,
116
+ )
117
+
118
+ params = model.get_params()
119
+ # Reset early-stopping / n_estimators for a fresh fit with eval tracking
120
+ params["n_estimators"] = min(params.get("n_estimators", 300) or 300, 500)
121
+ params["eval_metric"] = ["logloss", "error", "auc"]
122
+
123
+ clf = XGBClassifier(**{k: v for k, v in params.items() if k != "early_stopping_rounds"})
124
+ clf.fit(
125
+ X_tr, y_tr,
126
+ eval_set=[(X_tr, y_tr), (X_val, y_val)],
127
+ verbose=False,
128
+ )
129
+
130
+ history = clf.evals_result()
131
+ tr = history["validation_0"]
132
+ vl = history["validation_1"]
133
+
134
+ fig, axes = plt.subplots(1, 3, figsize=(16, 5))
135
+ x = np.arange(1, len(tr["logloss"]) + 1)
136
+
137
+ for ax, metric, title in [
138
+ (axes[0], "logloss", "Log Loss"),
139
+ (axes[1], "error", "Error Rate"),
140
+ (axes[2], "auc", "ROC-AUC"),
141
+ ]:
142
+ ax.plot(x, tr[metric], color=PALETTE["primary"], lw=2.2, label="Eğitim / Train")
143
+ ax.plot(x, vl[metric], color=PALETTE["error"], lw=2.2,
144
+ linestyle="--", label="Doğrulama / Validation")
145
+ ax.set_xlabel("Boosting Round")
146
+ ax.set_ylabel(title)
147
+ ax.set_title(f"{title} — Boosting İlerlemesi", fontweight="bold")
148
+ ax.legend(framealpha=0.85)
149
+ # best round annotation
150
+ best_idx = int(np.argmin(vl["logloss"])) if metric == "logloss" else int(np.argmax(vl[metric]))
151
+ ax.axvline(best_idx + 1, color=PALETTE["accent"], linestyle=":", alpha=0.7)
152
+ ax.annotate(
153
+ f"en iyi: {best_idx + 1}",
154
+ xy=(best_idx + 1, vl[metric][best_idx]),
155
+ xytext=(12, -12), textcoords="offset points",
156
+ fontsize=9, color=PALETTE["fg"],
157
+ )
158
+
159
+ fig.suptitle("XGBoost Eğitim Geçmişi — Train vs Validation", fontsize=14, fontweight="bold")
160
+ plt.tight_layout()
161
+ plt.savefig(FIGURES_DIR / "training_history.png")
162
+ plt.close()
163
+ print(" ✓ training_history.png")
164
+
165
+
166
+ # ── 2. Per-class precision/recall/F1 ─────────────────────────────────────
167
+ def fig_per_class_metrics(y_true, y_pred):
168
+ p, r, f, support = precision_recall_fscore_support(y_true, y_pred)
169
+ classes = ["İnsan / Human", "AI / Yapay"]
170
+ metrics = {"Precision": p, "Recall": r, "F1 Score": f}
171
+
172
+ fig, ax = plt.subplots(figsize=(9, 6))
173
+ x = np.arange(len(classes))
174
+ width = 0.25
175
+ colors = [PALETTE["primary"], PALETTE["secondary"], PALETTE["error"]]
176
+
177
+ for i, (name, vals) in enumerate(metrics.items()):
178
+ bars = ax.bar(x + (i - 1) * width, vals, width, label=name,
179
+ color=colors[i], edgecolor=PALETTE["fg"], linewidth=0.5)
180
+ for bar, v in zip(bars, vals):
181
+ ax.text(bar.get_x() + bar.get_width() / 2, v + 0.01,
182
+ f"{v:.3f}", ha="center", va="bottom", fontsize=10, fontweight="bold")
183
+
184
+ ax.set_xticks(x)
185
+ ax.set_xticklabels([f"{c}\n(n={s})" for c, s in zip(classes, support)])
186
+ ax.set_ylabel("Skor / Score")
187
+ ax.set_title("Sınıf Başına Performans — Precision / Recall / F1",
188
+ fontsize=13, fontweight="bold")
189
+ ax.set_ylim([0, 1.08])
190
+ ax.legend(loc="lower right", framealpha=0.85)
191
+ plt.savefig(FIGURES_DIR / "per_class_metrics.png")
192
+ plt.close()
193
+ print(" ✓ per_class_metrics.png")
194
+
195
+
196
+ # ── 3. Learning curve (score vs training set size) ───────────────────────
197
+ def fig_learning_curve(model, scaler, X, y):
198
+ X_scaled = scaler.transform(X)
199
+ train_sizes = np.linspace(0.1, 1.0, 6)
200
+ cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
201
+
202
+ sizes, tr_scores, val_scores = learning_curve(
203
+ clone(model), X_scaled, y,
204
+ train_sizes=train_sizes, cv=cv,
205
+ scoring="roc_auc", n_jobs=-1,
206
+ random_state=42,
207
+ )
208
+ tr_mean, tr_std = tr_scores.mean(1), tr_scores.std(1)
209
+ val_mean, val_std = val_scores.mean(1), val_scores.std(1)
210
+
211
+ fig, ax = plt.subplots(figsize=(9, 6.5))
212
+ ax.plot(sizes, tr_mean, "o-", color=PALETTE["primary"], lw=2.5, label="Eğitim / Train")
213
+ ax.fill_between(sizes, tr_mean - tr_std, tr_mean + tr_std,
214
+ alpha=0.18, color=PALETTE["primary"])
215
+ ax.plot(sizes, val_mean, "s-", color=PALETTE["error"], lw=2.5,
216
+ label="Çapraz Doğrulama / Cross-Validation")
217
+ ax.fill_between(sizes, val_mean - val_std, val_mean + val_std,
218
+ alpha=0.18, color=PALETTE["error"])
219
+
220
+ ax.set_xlabel("Eğitim Örneği Sayısı / Training Examples")
221
+ ax.set_ylabel("ROC-AUC")
222
+ ax.set_title("Öğrenme Eğrisi — Model Veri ile Öğreniyor mu?",
223
+ fontsize=13, fontweight="bold")
224
+ ax.legend(loc="lower right", framealpha=0.85)
225
+ gap = tr_mean[-1] - val_mean[-1]
226
+ diagnosis = "düşük varyans (iyi)" if gap < 0.03 else "overfitting işareti"
227
+ ax.annotate(
228
+ f"Son gap: {gap:.4f}\n→ {diagnosis}",
229
+ xy=(0.55, 0.05), xycoords="axes fraction",
230
+ fontsize=10,
231
+ bbox=dict(boxstyle="round,pad=0.5", facecolor=PALETTE["bg"],
232
+ edgecolor=PALETTE["primary"], alpha=0.85),
233
+ )
234
+ plt.savefig(FIGURES_DIR / "learning_curve.png")
235
+ plt.close()
236
+ print(" ✓ learning_curve.png")
237
+
238
+
239
+ # ── 4. Threshold sweep ───────────────────────────────────────────────────
240
+ def fig_threshold_sweep(y_true, y_prob):
241
+ thresholds = np.linspace(0.05, 0.95, 91)
242
+ precisions, recalls, f1s = [], [], []
243
+ for t in thresholds:
244
+ pred = (y_prob > t).astype(int)
245
+ precisions.append(precision_score(y_true, pred, zero_division=0))
246
+ recalls.append(recall_score(y_true, pred, zero_division=0))
247
+ f1s.append(f1_score(y_true, pred, zero_division=0))
248
+
249
+ precisions, recalls, f1s = np.array(precisions), np.array(recalls), np.array(f1s)
250
+ best_idx = int(np.argmax(f1s))
251
+ best_t = thresholds[best_idx]
252
+
253
+ fig, ax = plt.subplots(figsize=(10, 6))
254
+ ax.plot(thresholds, precisions, color=PALETTE["primary"], lw=2.5, label="Precision")
255
+ ax.plot(thresholds, recalls, color=PALETTE["secondary"], lw=2.5, label="Recall")
256
+ ax.plot(thresholds, f1s, color=PALETTE["error"], lw=2.8, label="F1 Score")
257
+ ax.axvline(0.5, color=PALETTE["fg"], linestyle=":", alpha=0.5, label="Varsayılan 0.5")
258
+ ax.axvline(best_t, color=PALETTE["accent"], linestyle="--", lw=2,
259
+ label=f"En iyi F1 @ {best_t:.2f}")
260
+ ax.scatter([best_t], [f1s[best_idx]], color=PALETTE["accent"],
261
+ s=100, zorder=5, edgecolor=PALETTE["fg"])
262
+
263
+ ax.set_xlabel("Karar Eşiği / Decision Threshold")
264
+ ax.set_ylabel("Skor")
265
+ ax.set_title("Eşik Taraması — Precision / Recall / F1 vs Threshold",
266
+ fontsize=13, fontweight="bold")
267
+ ax.legend(loc="lower left", framealpha=0.85)
268
+ ax.set_xlim([0, 1])
269
+ ax.set_ylim([0, 1.02])
270
+ plt.savefig(FIGURES_DIR / "threshold_sweep.png")
271
+ plt.close()
272
+ print(" ✓ threshold_sweep.png")
273
+
274
+
275
+ # ── 5. Score distribution histogram ──────────────────────────────────────
276
+ def fig_score_distribution(y_true, y_prob):
277
+ fig, ax = plt.subplots(figsize=(10, 6))
278
+ bins = np.linspace(0, 1, 41)
279
+ human_probs = y_prob[y_true == 0]
280
+ ai_probs = y_prob[y_true == 1]
281
+
282
+ ax.hist(human_probs, bins=bins, alpha=0.65, color=PALETTE["human"],
283
+ label=f"İnsan (n={len(human_probs)})", edgecolor=PALETTE["fg"], linewidth=0.3)
284
+ ax.hist(ai_probs, bins=bins, alpha=0.65, color=PALETTE["ai"],
285
+ label=f"AI (n={len(ai_probs)})", edgecolor=PALETTE["fg"], linewidth=0.3)
286
+ ax.axvline(0.5, color=PALETTE["fg"], linestyle="--", alpha=0.7, lw=2,
287
+ label="Karar Eşiği")
288
+
289
+ ax.set_xlabel("Tahmin Olasılığı P(AI) / Predicted Probability")
290
+ ax.set_ylabel("Örnek Sayısı / Count")
291
+ ax.set_title("Tahmin Olasılığı Dağılımı — Sınıf Bazlı",
292
+ fontsize=13, fontweight="bold")
293
+ ax.legend(framealpha=0.85)
294
+ plt.savefig(FIGURES_DIR / "score_distribution.png")
295
+ plt.close()
296
+ print(" ✓ score_distribution.png")
297
+
298
+
299
+ # ── 6. Per-source breakdown ──────────────────────────────────────────────
300
+ def fig_per_source_performance(y_true, y_pred, paths, rows):
301
+ # Join features.csv by path with metadata.csv source info
302
+ if not METADATA_CSV.exists():
303
+ print(" ! metadata.csv missing, skipping per_source_performance")
304
+ return
305
+
306
+ with open(METADATA_CSV, "r", encoding="utf-8") as f:
307
+ meta_rows = list(csv.DictReader(f))
308
+ # normalize paths for join (forward slashes)
309
+ path_to_source = {
310
+ r["path"].replace("\\", "/"): r.get("source", "unknown")
311
+ for r in meta_rows
312
+ }
313
+
314
+ sources_hits: dict[str, dict[str, int]] = {}
315
+ for yt, yp, path in zip(y_true, y_pred, paths):
316
+ key = path.replace("\\", "/")
317
+ src = path_to_source.get(key, "unknown")
318
+ d = sources_hits.setdefault(src, {"total": 0, "correct": 0, "ai": 0, "human": 0})
319
+ d["total"] += 1
320
+ if yt == yp:
321
+ d["correct"] += 1
322
+ d["ai" if yt == 1 else "human"] += 1
323
+
324
+ sources = [s for s in sources_hits if sources_hits[s]["total"] >= 20]
325
+ sources.sort(key=lambda s: -sources_hits[s]["total"])
326
+ if not sources:
327
+ print(" ! no source has >=20 samples, skipping")
328
+ return
329
+
330
+ accs = [sources_hits[s]["correct"] / sources_hits[s]["total"] for s in sources]
331
+ totals = [sources_hits[s]["total"] for s in sources]
332
+
333
+ fig, ax = plt.subplots(figsize=(10, max(4, len(sources) * 0.45)))
334
+ y_pos = np.arange(len(sources))
335
+ colors = plt.cm.copper(np.linspace(0.3, 0.9, len(sources)))
336
+ ax.barh(y_pos, accs, color=colors, edgecolor=PALETTE["fg"], linewidth=0.5)
337
+ ax.set_yticks(y_pos)
338
+ ax.set_yticklabels([f"{s} (n={n})" for s, n in zip(sources, totals)])
339
+ ax.invert_yaxis()
340
+ ax.set_xlabel("Accuracy")
341
+ ax.set_title("Veri Kaynağı Bazlı Performans",
342
+ fontsize=13, fontweight="bold")
343
+ ax.set_xlim([0, 1.0])
344
+ for i, v in enumerate(accs):
345
+ ax.text(v + 0.005, i, f"{v:.3f}", va="center", fontsize=9)
346
+ plt.savefig(FIGURES_DIR / "per_source_performance.png")
347
+ plt.close()
348
+ print(" ✓ per_source_performance.png")
349
+
350
+
351
+ # ── 7. Classification report as styled table ─────────────────────────────
352
+ def fig_classification_report(y_true, y_pred):
353
+ from sklearn.metrics import classification_report
354
+ report = classification_report(
355
+ y_true, y_pred, target_names=["Human (İnsan)", "AI (Yapay)"],
356
+ digits=4, output_dict=True,
357
+ )
358
+
359
+ fig, ax = plt.subplots(figsize=(10, 4.5))
360
+ ax.axis("off")
361
+
362
+ classes = ["Human (İnsan)", "AI (Yapay)", "accuracy", "macro avg", "weighted avg"]
363
+ header = ["Class", "Precision", "Recall", "F1", "Support"]
364
+ data = [header]
365
+ for c in classes:
366
+ r = report.get(c, {})
367
+ if c == "accuracy":
368
+ data.append([c, "", "", f"{report['accuracy']:.4f}", f"{len(y_true)}"])
369
+ else:
370
+ data.append([
371
+ c,
372
+ f"{r.get('precision', 0):.4f}",
373
+ f"{r.get('recall', 0):.4f}",
374
+ f"{r.get('f1-score', 0):.4f}",
375
+ f"{int(r.get('support', 0))}",
376
+ ])
377
+
378
+ table = ax.table(
379
+ cellText=data, cellLoc="center", loc="center",
380
+ colWidths=[0.25, 0.18, 0.18, 0.18, 0.18],
381
+ )
382
+ table.auto_set_font_size(False)
383
+ table.set_fontsize(11)
384
+ table.scale(1, 1.8)
385
+
386
+ # header styling
387
+ for i in range(len(header)):
388
+ table[(0, i)].set_facecolor(PALETTE["primary"])
389
+ table[(0, i)].set_text_props(weight="bold", color=PALETTE["bg"])
390
+ # row stripes
391
+ for r in range(1, len(data)):
392
+ for c in range(len(header)):
393
+ table[(r, c)].set_facecolor(
394
+ PALETTE["bg"] if r % 2 else "#f0e6d0",
395
+ )
396
+ table[(r, c)].set_edgecolor(PALETTE["grid"])
397
+
398
+ ax.set_title("Sınıflandırma Raporu — 5-fold Cross-Validation",
399
+ fontsize=13, fontweight="bold", pad=18)
400
+ plt.savefig(FIGURES_DIR / "classification_report.png")
401
+ plt.close()
402
+ print(" ✓ classification_report.png")
403
+
404
+
405
+ def main():
406
+ FIGURES_DIR.mkdir(parents=True, exist_ok=True)
407
+ print(f"Output: {FIGURES_DIR}")
408
+ print("Loading...")
409
+ model, scaler, feature_cols, results = _load()
410
+ X, y, paths, rows = _load_data(feature_cols)
411
+
412
+ print("CV predictions (5-fold)...")
413
+ X_scaled = scaler.transform(X)
414
+ y_pred, y_prob = _cv_predict(model, X_scaled, y)
415
+
416
+ print("\nGenerating deep figures...")
417
+ fig_per_class_metrics(y, y_pred)
418
+ fig_threshold_sweep(y, y_prob)
419
+ fig_score_distribution(y, y_prob)
420
+ fig_per_source_performance(y, y_pred, paths, rows)
421
+ fig_classification_report(y, y_pred)
422
+ fig_training_history(model, scaler, X, y)
423
+ print("Learning curve (may take ~30s)...")
424
+ fig_learning_curve(model, scaler, X, y)
425
+
426
+ total = len(list(FIGURES_DIR.glob("*.png")))
427
+ print(f"\nDone. Total figures in {FIGURES_DIR}: {total}")
428
+
429
+
430
+ if __name__ == "__main__":
431
+ main()