Rthur2003 commited on
Commit
b8d143b
·
1 Parent(s): edc551d

feat: add deep learning classifier comparison script for AURIS with multiple architectures

Browse files
app/training/train_deep_classifiers.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Deep learning classifier comparison for AURIS.
3
+
4
+ Trains and evaluates multiple neural network architectures on
5
+ the 47 extracted audio features using stratified k-fold CV.
6
+
7
+ Architectures:
8
+ 1. Deep MLP (512-256-128-64) with BatchNorm + Dropout
9
+ 2. 1D-CNN on feature vector (treats features as 1D signal)
10
+ 3. Residual MLP (skip connections)
11
+ 4. Attention MLP (self-attention over feature groups)
12
+
13
+ Usage:
14
+ python -m app.training.train_deep_classifiers ../DataSet/features.csv
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import csv
20
+ import json
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ import numpy as np
26
+ import torch
27
+ import torch.nn as nn
28
+ from torch.utils.data import DataLoader, TensorDataset
29
+ from sklearn.model_selection import StratifiedKFold
30
+ from sklearn.preprocessing import StandardScaler
31
+ from sklearn.metrics import accuracy_score, roc_auc_score, f1_score, precision_score, recall_score
32
+
33
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34
+ SEED = 42
35
+ N_FOLDS = 5
36
+ EPOCHS = 100
37
+ PATIENCE = 10
38
+ BATCH_SIZE = 64
39
+ LR = 1e-3
40
+
41
+
42
+ def set_seed(seed: int = SEED) -> None:
43
+ np.random.seed(seed)
44
+ torch.manual_seed(seed)
45
+ if torch.cuda.is_available():
46
+ torch.cuda.manual_seed_all(seed)
47
+
48
+
49
+ class DeepMLP(nn.Module):
50
+ def __init__(self, n_features: int) -> None:
51
+ super().__init__()
52
+ self.net = nn.Sequential(
53
+ nn.Linear(n_features, 512),
54
+ nn.BatchNorm1d(512),
55
+ nn.ReLU(),
56
+ nn.Dropout(0.4),
57
+ nn.Linear(512, 256),
58
+ nn.BatchNorm1d(256),
59
+ nn.ReLU(),
60
+ nn.Dropout(0.3),
61
+ nn.Linear(256, 128),
62
+ nn.BatchNorm1d(128),
63
+ nn.ReLU(),
64
+ nn.Dropout(0.2),
65
+ nn.Linear(128, 64),
66
+ nn.ReLU(),
67
+ nn.Linear(64, 1),
68
+ )
69
+
70
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
71
+ return self.net(x).squeeze(-1)
72
+
73
+
74
+ class Conv1DClassifier(nn.Module):
75
+ def __init__(self, n_features: int) -> None:
76
+ super().__init__()
77
+ self.conv = nn.Sequential(
78
+ nn.Conv1d(1, 64, kernel_size=5, padding=2),
79
+ nn.BatchNorm1d(64),
80
+ nn.ReLU(),
81
+ nn.Conv1d(64, 128, kernel_size=3, padding=1),
82
+ nn.BatchNorm1d(128),
83
+ nn.ReLU(),
84
+ nn.AdaptiveAvgPool1d(1),
85
+ )
86
+ self.fc = nn.Sequential(
87
+ nn.Linear(128, 64),
88
+ nn.ReLU(),
89
+ nn.Dropout(0.3),
90
+ nn.Linear(64, 1),
91
+ )
92
+
93
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
94
+ x = x.unsqueeze(1) # (B, 1, F)
95
+ x = self.conv(x).squeeze(-1) # (B, 128)
96
+ return self.fc(x).squeeze(-1)
97
+
98
+
99
+ class ResidualBlock(nn.Module):
100
+ def __init__(self, dim: int, dropout: float = 0.2) -> None:
101
+ super().__init__()
102
+ self.block = nn.Sequential(
103
+ nn.Linear(dim, dim),
104
+ nn.BatchNorm1d(dim),
105
+ nn.ReLU(),
106
+ nn.Dropout(dropout),
107
+ nn.Linear(dim, dim),
108
+ nn.BatchNorm1d(dim),
109
+ )
110
+ self.relu = nn.ReLU()
111
+
112
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
113
+ return self.relu(x + self.block(x))
114
+
115
+
116
+ class ResidualMLP(nn.Module):
117
+ def __init__(self, n_features: int) -> None:
118
+ super().__init__()
119
+ self.input_proj = nn.Sequential(
120
+ nn.Linear(n_features, 256),
121
+ nn.BatchNorm1d(256),
122
+ nn.ReLU(),
123
+ )
124
+ self.res_blocks = nn.Sequential(
125
+ ResidualBlock(256, 0.3),
126
+ ResidualBlock(256, 0.2),
127
+ ResidualBlock(256, 0.1),
128
+ )
129
+ self.head = nn.Sequential(
130
+ nn.Linear(256, 64),
131
+ nn.ReLU(),
132
+ nn.Linear(64, 1),
133
+ )
134
+
135
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
136
+ x = self.input_proj(x)
137
+ x = self.res_blocks(x)
138
+ return self.head(x).squeeze(-1)
139
+
140
+
141
+ class AttentionMLP(nn.Module):
142
+ def __init__(self, n_features: int) -> None:
143
+ super().__init__()
144
+ self.proj = nn.Linear(n_features, 256)
145
+ self.attn = nn.MultiheadAttention(256, num_heads=4, batch_first=True)
146
+ self.norm = nn.LayerNorm(256)
147
+ self.head = nn.Sequential(
148
+ nn.Linear(256, 128),
149
+ nn.ReLU(),
150
+ nn.Dropout(0.3),
151
+ nn.Linear(128, 1),
152
+ )
153
+
154
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
155
+ x = self.proj(x)
156
+ x = x.unsqueeze(1) # (B, 1, 256)
157
+ x_chunk = x.expand(-1, 4, -1) # (B, 4, 256) - create sequence
158
+ attn_out, _ = self.attn(x_chunk, x_chunk, x_chunk)
159
+ x = self.norm(attn_out.mean(dim=1)) # (B, 256)
160
+ return self.head(x).squeeze(-1)
161
+
162
+
163
+ def load_data(csv_path: str | Path) -> tuple[np.ndarray, np.ndarray, list[str]]:
164
+ _EXCLUDE = {"file_path", "label_int", "duration_sec", "sample_rate"}
165
+ rows, labels = [], []
166
+ with open(csv_path, "r", encoding="utf-8") as f:
167
+ reader = csv.DictReader(f)
168
+ feature_cols = [c for c in reader.fieldnames if c not in _EXCLUDE]
169
+ for row in reader:
170
+ vals = []
171
+ for col in feature_cols:
172
+ try:
173
+ vals.append(float(row[col]))
174
+ except (ValueError, KeyError):
175
+ vals.append(0.0)
176
+ rows.append(vals)
177
+ labels.append(int(row["label_int"]))
178
+ X = np.nan_to_num(np.array(rows, dtype=np.float32), nan=0.0)
179
+ y = np.array(labels, dtype=np.int32)
180
+ return X, y, feature_cols
181
+
182
+
183
+ def train_one_fold(
184
+ model: nn.Module,
185
+ X_train: np.ndarray, y_train: np.ndarray,
186
+ X_val: np.ndarray, y_val: np.ndarray,
187
+ ) -> tuple[float, np.ndarray]:
188
+ scaler = StandardScaler()
189
+ X_tr = scaler.fit_transform(X_train)
190
+ X_v = scaler.transform(X_val)
191
+
192
+ train_ds = TensorDataset(
193
+ torch.tensor(X_tr, dtype=torch.float32),
194
+ torch.tensor(y_train, dtype=torch.float32),
195
+ )
196
+ val_X = torch.tensor(X_v, dtype=torch.float32).to(DEVICE)
197
+ val_y = torch.tensor(y_val, dtype=torch.float32)
198
+
199
+ loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)
200
+ model = model.to(DEVICE)
201
+ optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4)
202
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
203
+ optimizer, mode="max", factor=0.5, patience=5
204
+ )
205
+ criterion = nn.BCEWithLogitsLoss()
206
+
207
+ best_auc = 0.0
208
+ best_probs = None
209
+ patience_ctr = 0
210
+
211
+ for epoch in range(EPOCHS):
212
+ model.train()
213
+ for bx, by in loader:
214
+ bx, by = bx.to(DEVICE), by.to(DEVICE)
215
+ optimizer.zero_grad()
216
+ logits = model(bx)
217
+ loss = criterion(logits, by)
218
+ loss.backward()
219
+ optimizer.step()
220
+
221
+ model.eval()
222
+ with torch.no_grad():
223
+ v_logits = model(val_X)
224
+ v_probs = torch.sigmoid(v_logits).cpu().numpy()
225
+
226
+ auc = roc_auc_score(y_val, v_probs)
227
+ scheduler.step(auc)
228
+
229
+ if auc > best_auc:
230
+ best_auc = auc
231
+ best_probs = v_probs.copy()
232
+ patience_ctr = 0
233
+ else:
234
+ patience_ctr += 1
235
+ if patience_ctr >= PATIENCE:
236
+ break
237
+
238
+ return best_auc, best_probs
239
+
240
+
241
+ def evaluate_cv(
242
+ model_class: type,
243
+ X: np.ndarray, y: np.ndarray,
244
+ n_features: int,
245
+ ) -> dict:
246
+ cv = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=SEED)
247
+ all_probs = np.zeros(len(y))
248
+ aucs = []
249
+ t0 = time.time()
250
+
251
+ for fold, (train_idx, val_idx) in enumerate(cv.split(X, y)):
252
+ set_seed(SEED + fold)
253
+ model = model_class(n_features)
254
+ auc, probs = train_one_fold(
255
+ model,
256
+ X[train_idx], y[train_idx],
257
+ X[val_idx], y[val_idx],
258
+ )
259
+ all_probs[val_idx] = probs
260
+ aucs.append(auc)
261
+ print(f" Fold {fold+1}: AUC={auc:.4f}")
262
+
263
+ elapsed = time.time() - t0
264
+ y_pred = (all_probs > 0.5).astype(int)
265
+ return {
266
+ "accuracy": round(float(accuracy_score(y, y_pred)), 4),
267
+ "precision": round(float(precision_score(y, y_pred, zero_division=0)), 4),
268
+ "recall": round(float(recall_score(y, y_pred, zero_division=0)), 4),
269
+ "f1": round(float(f1_score(y, y_pred, zero_division=0)), 4),
270
+ "roc_auc": round(float(roc_auc_score(y, all_probs)), 4),
271
+ "fold_aucs": [round(a, 4) for a in aucs],
272
+ "train_time_sec": round(elapsed, 1),
273
+ }
274
+
275
+
276
+ def main() -> None:
277
+ csv_path = sys.argv[1] if len(sys.argv) > 1 else "../DataSet/features.csv"
278
+ print(f"Device: {DEVICE}")
279
+ print(f"Loading: {csv_path}")
280
+
281
+ X, y, feature_cols = load_data(csv_path)
282
+ n_features = X.shape[1]
283
+ print(f"Samples: {len(y)}, Features: {n_features}")
284
+ print(f"AI: {np.sum(y == 1)}, Human: {np.sum(y == 0)}")
285
+
286
+ models = {
287
+ "Deep MLP (512-256-128-64)": DeepMLP,
288
+ "1D-CNN": Conv1DClassifier,
289
+ "Residual MLP (3 blocks)": ResidualMLP,
290
+ "Attention MLP": AttentionMLP,
291
+ }
292
+
293
+ all_results = {}
294
+ for name, cls in models.items():
295
+ print(f"\n{'='*60}")
296
+ print(f" {name}")
297
+ print(f"{'='*60}")
298
+ result = evaluate_cv(cls, X, y, n_features)
299
+ all_results[name] = result
300
+ print(f" => Acc={result['accuracy']:.4f} AUC={result['roc_auc']:.4f} "
301
+ f"F1={result['f1']:.4f} Time={result['train_time_sec']:.0f}s")
302
+
303
+ out_path = Path("models/deep_learning_results.json")
304
+ with open(out_path, "w") as f:
305
+ json.dump(all_results, f, indent=2)
306
+ print(f"\nResults saved: {out_path}")
307
+
308
+ print(f"\n{'='*60}")
309
+ print(" SUMMARY")
310
+ print(f"{'='*60}")
311
+ for name, r in sorted(all_results.items(), key=lambda x: -x[1]["roc_auc"]):
312
+ print(f" {name:35s} AUC={r['roc_auc']:.4f} Acc={r['accuracy']:.4f}")
313
+
314
+
315
+ if __name__ == "__main__":
316
+ main()