ducdatit2002 commited on
Commit
e58cdae
·
verified ·
1 Parent(s): f44dc24

Upload folder using huggingface_hub

Browse files
CNN-Autoencoder.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.preprocessing import MinMaxScaler
5
+ from sklearn.model_selection import StratifiedShuffleSplit
6
+ from sklearn.metrics import (
7
+ confusion_matrix,
8
+ precision_score,
9
+ recall_score,
10
+ f1_score,
11
+ accuracy_score,
12
+ classification_report
13
+ )
14
+ import torch
15
+ import torch.nn as nn
16
+ from torch.utils.data import Dataset, DataLoader, Subset
17
+
18
+ # ============================================
19
+ # 1. CÀI ĐẶT THAM SỐ CHUNG
20
+ # ============================================
21
+
22
+ DATA_PATH = "dataset.xlsx"
23
+
24
+ FEATURE_COLUMNS = [
25
+ "Temp",
26
+ "Turbidity (cm)",
27
+ "DO(mg/L)",
28
+ "BOD (mg/L)",
29
+ "CO2",
30
+ "pH`",
31
+ "Alkalinity (mg L-1 )",
32
+ "Hardness (mg L-1 )",
33
+ "Calcium (mg L-1 )",
34
+ "Ammonia (mg L-1 )",
35
+ "Nitrite (mg L-1 )",
36
+ "Phosphorus (mg L-1 )",
37
+ "H2S (mg L-1 )",
38
+ "Plankton (No. L-1)"
39
+ ]
40
+ LABEL_COL = "Water Quality"
41
+
42
+ SEQUENCE_LENGTH = 10
43
+
44
+ TRAIN_RATIO = 0.8
45
+ VAL_RATIO = 0.1
46
+ TEST_RATIO = 0.1
47
+
48
+ # CNN-AE hyperparameters
49
+ INPUT_DIM = len(FEATURE_COLUMNS) # 14 features
50
+ SEQ_LEN = SEQUENCE_LENGTH # 10 time steps
51
+ CHANNELS = INPUT_DIM # treat each feature as a channel
52
+ AE_LR = 1e-3
53
+ AE_EPOCHS = 50
54
+ BATCH_SIZE = 64
55
+ RANDOM_STATE = 42
56
+
57
+ THRESHOLD_STD_FACTOR = 2 # threshold = mean + 2*std on validation normal
58
+
59
+ # ============================================
60
+ # 2. ĐỌC VÀ TIỀN XỬ LÝ DỮ LIỆU
61
+ # ============================================
62
+
63
+ df = pd.read_excel(DATA_PATH)
64
+ df = df.dropna(how="all")
65
+
66
+ # Đảm bảo label là int
67
+ df[LABEL_COL] = df[LABEL_COL].astype(int)
68
+ labels_all = df[LABEL_COL].values # shape = (num_total,)
69
+
70
+ # Chuyển dấu phẩy sang dấu chấm, convert các cột tính sang float
71
+ for col in FEATURE_COLUMNS:
72
+ if df[col].dtype == object or df[col].dtype == str:
73
+ df[col] = df[col].apply(lambda x: str(x).replace(",", "."))
74
+ df[col] = df[col].astype(float)
75
+
76
+ data_raw = df[FEATURE_COLUMNS].values # shape = (num_total, 14)
77
+
78
+ # Chuẩn hóa min-max
79
+ scaler = MinMaxScaler()
80
+ data_scaled = scaler.fit_transform(data_raw) # shape = (num_total, 14)
81
+
82
+ # ============================================
83
+ # 3. DATASET CHO TIME-SERIES
84
+ # ============================================
85
+
86
+ class CNNTimeSeriesDataset(Dataset):
87
+ """
88
+ Trả về x_window: shape (channels, seq_len)
89
+ Mỗi channel tương ứng một feature.
90
+ """
91
+ def __init__(self, data, seq_len):
92
+ self.data = data
93
+ self.seq_len = seq_len
94
+ self.num_items = data.shape[0] - seq_len
95
+
96
+ def __len__(self):
97
+ return self.num_items
98
+
99
+ def __getitem__(self, idx):
100
+ window = self.data[idx : idx + self.seq_len] # shape = (seq_len, features)
101
+ # transpose thành (features, seq_len) để input cho Conv1d
102
+ x = window.T # (channels, seq_len)
103
+ return torch.tensor(x, dtype=torch.float32)
104
+
105
+
106
+ num_total = data_scaled.shape[0]
107
+ num_items = num_total - SEQUENCE_LENGTH
108
+
109
+ # Tạo mảng y_seq: nhãn tại cuối mỗi window
110
+ y_seq = np.zeros(num_items, dtype=int)
111
+ for i in range(num_items):
112
+ y_seq[i] = labels_all[i + SEQUENCE_LENGTH]
113
+
114
+ # ============================================
115
+ # 4. STRATIFIED SPLIT (TRAIN/VAL/TEST)
116
+ # ============================================
117
+
118
+ sss1 = StratifiedShuffleSplit(n_splits=1, test_size=TEST_RATIO, random_state=RANDOM_STATE)
119
+ for train_val_idx, test_idx in sss1.split(np.zeros(num_items), y_seq):
120
+ pass
121
+
122
+ val_size_rel = VAL_RATIO / (TRAIN_RATIO + VAL_RATIO)
123
+ sss2 = StratifiedShuffleSplit(n_splits=1, test_size=val_size_rel, random_state=RANDOM_STATE)
124
+ for train_idx_rel, val_idx_rel in sss2.split(np.zeros(len(train_val_idx)), y_seq[train_val_idx]):
125
+ pass
126
+
127
+ train_idx = train_val_idx[train_idx_rel]
128
+ val_idx = train_val_idx[val_idx_rel]
129
+
130
+ def count_labels(indices, y):
131
+ u, c = np.unique(y[indices], return_counts=True)
132
+ return dict(zip(u.tolist(), c.tolist()))
133
+
134
+ print("Train labels:", count_labels(train_idx, y_seq))
135
+ print("Val labels:", count_labels(val_idx, y_seq))
136
+ print("Test labels:", count_labels(test_idx, y_seq))
137
+
138
+ # ============================================
139
+ # 5. TẠO DATALOADER CHO AUTOENCODER (CHỈ DÙNG NORMAL)
140
+ # ============================================
141
+
142
+ dataset_all = CNNTimeSeriesDataset(data_scaled, SEQUENCE_LENGTH)
143
+
144
+ # Chỉ lấy index có nhãn 0 hoặc 1 cho train/val AE
145
+ train_normal_idx = [i for i in train_idx if y_seq[i] < 2]
146
+ val_normal_idx = [i for i in val_idx if y_seq[i] < 2]
147
+
148
+ train_ae_dataset = Subset(dataset_all, train_normal_idx)
149
+ val_ae_dataset = Subset(dataset_all, val_normal_idx)
150
+ test_dataset = Subset(dataset_all, test_idx)
151
+
152
+ train_ae_loader = DataLoader(train_ae_dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
153
+ val_ae_loader = DataLoader(val_ae_dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
154
+ test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
155
+
156
+ # ============================================
157
+ # 6. XÂY DỰNG LSTM‐CNN AUTOENCODER
158
+ # ============================================
159
+
160
+ class CNNAutoencoder(nn.Module):
161
+ def __init__(self, channels, seq_len):
162
+ super(CNNAutoencoder, self).__init__()
163
+ self.channels = channels
164
+ self.seq_len = seq_len
165
+
166
+ # Encoder: Conv1d layers
167
+ self.encoder = nn.Sequential(
168
+ nn.Conv1d(in_channels=channels, out_channels=32, kernel_size=3, padding=1),
169
+ nn.ReLU(),
170
+ nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1),
171
+ nn.ReLU(),
172
+ nn.Conv1d(in_channels=64, out_channels=128, kernel_size=3, padding=1),
173
+ nn.ReLU(),
174
+ # Giữ nguyên chiều seq_len nhưng tăng depth
175
+ )
176
+ # Decoder: ConvTranspose1d layers
177
+ self.decoder = nn.Sequential(
178
+ nn.ConvTranspose1d(in_channels=128, out_channels=64, kernel_size=3, padding=1),
179
+ nn.ReLU(),
180
+ nn.ConvTranspose1d(in_channels=64, out_channels=32, kernel_size=3, padding=1),
181
+ nn.ReLU(),
182
+ nn.ConvTranspose1d(in_channels=32, out_channels=channels, kernel_size=3, padding=1),
183
+ nn.Sigmoid() # output trong [0,1] do data đã chuẩn hóa
184
+ )
185
+
186
+ def forward(self, x):
187
+ """
188
+ x: (batch, channels, seq_len)
189
+ trả về x_recon: (batch, channels, seq_len)
190
+ """
191
+ z = self.encoder(x)
192
+ x_recon = self.decoder(z)
193
+ return x_recon
194
+
195
+
196
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
197
+ ae_model = CNNAutoencoder(channels=CHANNELS, seq_len=SEQ_LEN).to(device)
198
+
199
+ ae_criterion = nn.MSELoss()
200
+ ae_optimizer = torch.optim.Adam(ae_model.parameters(), lr=AE_LR)
201
+
202
+ # ============================================
203
+ # 7. HUẤN LUYỆN CNN‐AE
204
+ # ============================================
205
+
206
+ best_val_loss = float("inf")
207
+ best_ae_path = "best_cnn_ae.pth"
208
+
209
+ for epoch in range(1, AE_EPOCHS + 1):
210
+ ae_model.train()
211
+ train_loss_sum = 0.0
212
+ for x_batch in train_ae_loader:
213
+ # x_batch: (batch, channels, seq_len)
214
+ x_batch = x_batch.to(device)
215
+ ae_optimizer.zero_grad()
216
+ x_recon = ae_model(x_batch)
217
+ loss = ae_criterion(x_recon, x_batch)
218
+ loss.backward()
219
+ ae_optimizer.step()
220
+ train_loss_sum += loss.item() * x_batch.size(0)
221
+ train_loss = train_loss_sum / len(train_ae_loader.dataset)
222
+
223
+ ae_model.eval()
224
+ val_loss_sum = 0.0
225
+ with torch.no_grad():
226
+ for x_batch in val_ae_loader:
227
+ x_batch = x_batch.to(device)
228
+ x_recon = ae_model(x_batch)
229
+ loss = ae_criterion(x_recon, x_batch)
230
+ val_loss_sum += loss.item() * x_batch.size(0)
231
+ val_loss = val_loss_sum / len(val_ae_loader.dataset)
232
+
233
+ print(f"Epoch {epoch:02d} | AE Train Loss: {train_loss:.6f} | AE Val Loss: {val_loss:.6f}")
234
+ if val_loss < best_val_loss:
235
+ best_val_loss = val_loss
236
+ torch.save(ae_model.state_dict(), best_ae_path)
237
+
238
+ ae_model.load_state_dict(torch.load(best_ae_path, map_location=device))
239
+
240
+
241
+ # ============================================
242
+ # 8. TÍNH RECONSTRUCTION ERROR TRÊN VALIDATION NORMAL
243
+ # ============================================
244
+
245
+ val_norm_errors = []
246
+ ae_model.eval()
247
+ with torch.no_grad():
248
+ for x_batch in val_ae_loader:
249
+ x_batch = x_batch.to(device)
250
+ x_recon = ae_model(x_batch)
251
+ # MSE dọc (channels x seq_len) cho mỗi sample
252
+ batch_errors = torch.mean((x_recon - x_batch) ** 2, dim=(1, 2))
253
+ val_norm_errors.append(batch_errors.cpu().numpy())
254
+ val_norm_errors = np.concatenate(val_norm_errors, axis=0)
255
+
256
+ mu_val = np.mean(val_norm_errors)
257
+ sigma_val = np.std(val_norm_errors)
258
+ threshold = mu_val + THRESHOLD_STD_FACTOR * sigma_val
259
+
260
+ print(f"\nThreshold (mean + {THRESHOLD_STD_FACTOR}*std) từ validation normal: {threshold:.6f}")
261
+
262
+
263
+ # ============================================
264
+ # 9. TÍNH RECONSTRUCTION ERROR TRÊN TEST & PHÁT HIỆN BẤT THƯỜNG
265
+ # ============================================
266
+
267
+ test_errors = []
268
+ ae_model.eval()
269
+ with torch.no_grad():
270
+ for x_batch in test_loader:
271
+ x_batch = x_batch.to(device)
272
+ x_recon = ae_model(x_batch)
273
+ batch_errors = torch.mean((x_recon - x_batch) ** 2, dim=(1, 2))
274
+ test_errors.append(batch_errors.cpu().numpy())
275
+ test_errors = np.concatenate(test_errors, axis=0)
276
+
277
+ anomalies = test_errors > threshold
278
+ num_anomalies = np.sum(anomalies)
279
+ print(f"Phát hiện {num_anomalies} samples bất thường trong tập test (trên tổng {len(test_errors)})")
280
+ print("Chỉ số sample bất thường (relative to test set):", np.where(anomalies)[0])
281
+
282
+
283
+ # ============================================
284
+ # 10. ĐÁNH GIÁ KẾT QUẢ
285
+ # ============================================
286
+
287
+ y_true = []
288
+ for idx in test_idx:
289
+ y_true.append(1 if labels_all[idx + SEQUENCE_LENGTH] == 2 else 0)
290
+ y_true = np.array(y_true, dtype=int)
291
+ y_pred = anomalies.astype(int)
292
+
293
+ cm = confusion_matrix(y_true, y_pred)
294
+ tn, fp, fn, tp = cm.ravel()
295
+
296
+ precision = precision_score(y_true, y_pred, zero_division=0)
297
+ recall = recall_score(y_true, y_pred, zero_division=0)
298
+ f1 = f1_score(y_true, y_pred, zero_division=0)
299
+ accuracy = accuracy_score(y_true, y_pred)
300
+
301
+ print("\n=== Confusion Matrix ===")
302
+ print(cm)
303
+ print(f"TN: {tn}, FP: {fp}")
304
+ print(f"FN: {fn}, TP: {tp}\n")
305
+
306
+ print("=== Metrics for Anomaly Detection ===")
307
+ print(f"Accuracy : {accuracy:.4f}")
308
+ print(f"Precision: {precision:.4f}")
309
+ print(f"Recall : {recall:.4f}")
310
+ print(f"F1-score : {f1:.4f}\n")
311
+
312
+ print("=== Classification Report ===")
313
+ print(
314
+ classification_report(
315
+ y_true,
316
+ y_pred,
317
+ target_names=["Normal (0)", "Anomaly (1)"],
318
+ zero_division=0
319
+ )
320
+ )
Report/CNN-Autoencoder.txt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Threshold (mean + 2*std) từ validation normal: 0.002760
2
+ Phát hiện 131 samples bất thường trong tập test (trên tổng 429)
3
+ Chỉ số sample bất thường (relative to test set): [ 4 7 10 11 14 15 21 23 24 27 32 33 36 38 42 47 49 53
4
+ 56 58 59 61 64 67 71 72 74 78 79 87 90 92 93 98 102 104
5
+ 107 111 114 115 116 120 121 124 125 128 130 132 138 141 149 150 152 154
6
+ 155 156 157 162 170 174 177 178 189 191 195 200 204 207 208 209 210 213
7
+ 214 215 216 217 221 225 232 234 235 236 238 239 246 248 250 261 267 271
8
+ 274 279 280 293 297 299 302 312 315 318 320 323 326 327 332 340 341 346
9
+ 351 352 359 360 366 368 374 378 380 386 387 388 392 393 394 405 406 409
10
+ 410 416 418 424 427]
11
+
12
+ === Confusion Matrix ===
13
+ [[279 1]
14
+ [ 19 130]]
15
+ TN: 279, FP: 1
16
+ FN: 19, TP: 130
17
+
18
+ === Metrics for Anomaly Detection ===
19
+ Accuracy : 0.9534
20
+ Precision: 0.9924
21
+ Recall : 0.8725
22
+ F1-score : 0.9286
23
+
24
+ === Classification Report ===
25
+ precision recall f1-score support
26
+
27
+ Normal (0) 0.94 1.00 0.97 280
28
+ Anomaly (1) 0.99 0.87 0.93 149
29
+
30
+ accuracy 0.95 429
31
+ macro avg 0.96 0.93 0.95 429
32
+ weighted avg 0.96 0.95 0.95 429
Report/Fully‐Connected-Sequence-Autoencoder.txt ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Threshold (mean + 2*std) từ tập validation normal: 0.024559
2
+ Phát hiện 109 samples bất thường trong tập test (trên tổng 429)
3
+ Chỉ số sample bất thường (relative to test set): [ 7, 10, 11, …, 427] (tổng 109 index)
4
+
5
+ === Confusion Matrix ===
6
+ [[279 1]
7
+ [ 41 108]]
8
+ TN: 279, FP: 1
9
+ FN: 41, TP: 108
10
+
11
+ === Metrics for Anomaly Detection ===
12
+ Accuracy : 0.9021
13
+ Precision: 0.9908
14
+ Recall : 0.7248
15
+ F1-score : 0.8372
16
+
17
+ === Classification Report ===
18
+ precision recall f1-score support
19
+
20
+ Normal (0) 0.87 1.00 0.93 280
21
+ Anomaly (1) 0.99 0.72 0.84 149
22
+
23
+ accuracy 0.90 429
24
+ macro avg 0.93 0.86 0.88 429
25
+ weighted avg 0.91 0.90 0.90 429
26
+
27
+
28
+ Fully‐Connected Sequence Autoencoder
29
+ Mục đích: Tái tạo (reconstruct) toàn bộ chuỗi con (window) đầu vào và coi sample nào tái tạo kém (error cao) là bất thường.
30
+
31
+ Kiến trúc:
32
+
33
+ Flatten sequence: Mỗi window có shape (seq_len, input_dim) = (10, 14). Ta “dẹt” thành vector độ dài 140 trước khi đưa vào mạng.
34
+
35
+ Encoder:
36
+
37
+ Input: vector 140 chiều.
38
+
39
+ Qua một lớp Linear(140 → 64), activation ReLU, rồi Linear(64 → latent_dim=64), ReLU → ra vector z chiều 64.
40
+
41
+ Decoder:
42
+
43
+ Nhận latent z (64 chiều), qua Linear(64 → 64), ReLU, rồi Linear(64 → 140) → output 140 chiều.
44
+
45
+ Cuối cùng reshape về (batch, seq_len, input_dim) = (batch, 10, 14) để so sánh với input gốc.
46
+
47
+ Cách train:
48
+
49
+ Chỉ dùng các sequence bình thường (nhãn 0 hoặc 1) trong phần train/val để huấn luyện autoencoder, với loss MSE giữa x_recon và x_orig.
50
+
51
+ Mục tiêu cho AE: học cách nén và tái tạo chính xác chuỗi “bình thường”.
52
+
53
+ Phát hiện bất thường:
54
+
55
+ Trên tập validation chỉ chứa normal, tính MSE (reconstruction error) cho mỗi sequence, lấy mean và std, rồi định nghĩa threshold = mean + k·std (ở code là k=2).
56
+
57
+ Trên tập test (có cả normal và anomalous), tính reconstruction error cho mỗi window.
58
+
59
+ Mỗi window có error > threshold được gán “anomaly”.
60
+
61
+ Ưu điểm so với LSTM Forecaster:
62
+
63
+ AE không cần dự báo chính xác bước kế tiếp, chỉ tập trung học cách tái tạo cả window “bình thường”. Bất cứ pattern lạ nào (anomaly) khiến lỗi tái tạo tăng cao.
64
+
65
+ Thích hợp khi cấu trúc anomaly làm cho AE khó tái tạo (ví dụ ngoại lai nằm ngoài phân phối normal).
66
+
67
+ Nhược điểm:
68
+
69
+ Mạng chỉ là fully‐connected – không tận dụng mối liên hệ tuần tự phức tạp như LSTM.
70
+
71
+ Kết quả phụ thuộc vào khả năng AE nén được đặc trưng chuỗi bình thường.
Report/LSTM-Forecaster.txt ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Test Loss: 0.020916
2
+ Threshold (mean + 2*std): 0.056502
3
+ Phát hiện 17 samples bất thường trong tập test (trên tổng 429)
4
+ Chỉ số sample bất thường (relative to test set): [ 7 15 53 56 78 132 204 213 216 232 236 238 318 323 346 368 427]
5
+
6
+ === Confusion Matrix ===
7
+ [[280 0]
8
+ [132 17]]
9
+ TN: 280, FP: 0
10
+ FN: 132, TP: 17
11
+
12
+ === Metrics for Anomaly Detection ===
13
+ Accuracy : 0.6923
14
+ Precision: 1.0000
15
+ Recall : 0.1141
16
+ F1-score : 0.2048
17
+
18
+ === Classification Report ===
19
+ precision recall f1-score support
20
+
21
+ Normal (0) 0.68 1.00 0.81 280
22
+ Anomaly (1) 1.00 0.11 0.20 149
23
+
24
+ accuracy 0.69 429
25
+ macro avg 0.84 0.56 0.51 429
26
+ weighted avg 0.79 0.69 0.60 429
27
+
28
+
29
+ LSTM Forecaster
30
+ Mục đích: Dự báo (forecast) giá trị multivariate của bước thời gian kế tiếp dựa trên một cửa sổ (window) gồm SEQUENCE_LENGTH bước trước đó.
31
+
32
+ Kiến trúc:
33
+
34
+ Một lớp LSTM với input_size = 14 (14 đặc trưng), hidden_size = 64, num_layers = 2, batch_first=True.
35
+
36
+ Sau khi LSTM chạy xong, ta lấy hidden state cuối cùng (hn[-1]) và đưa qua một lớp Linear (hidden_size → input_size) để sinh ra dự báo 14 chiều cho bước thời gian kế tiếp.
37
+
38
+ Sử dụng để phát hiện bất thường:
39
+
40
+ Huấn luyện LSTM trên toàn bộ dữ liệu (không phân biệt normal/anomaly), để nó học cách dự báo tín hiệu “bình thường” tốt nhất.
41
+
42
+ Trên tập test, với mỗi sequence, ta so sánh y_pred = model(x_window) với giá trị thực y_true. Tính lỗi MSE riêng cho từng sample.
43
+
44
+ Chọn một threshold (ví dụ mean + 2·std của tất cả các lỗi trên tập validation hoặc train), và gán những sample có lỗi > threshold là anomaly.
45
+
46
+ Ưu điểm: dễ implement, tận dụng đặc tính dãy thời gian. Nhược điểm: nếu signal biến động quá phức tạp thì LSTM dự báo chưa tốt, lỗi tái tạo không đủ “nhạy” để phát hiện anomaly.
47
+
Report/transformer_ae_results.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Test Loss: 0.021181
2
+ Threshold (mean + 2*std): 0.032068
3
+ Phát hiện 65 samples bất thường trên tổng 429
4
+ Chỉ số sample bất thường: [ 7 10 15 56 61 67 71 72 74 79 98 107 114 121 124 125 128 132
5
+ 138 149 150 155 157 170 177 189 191 200 204 207 208 210 213 214 216 221
6
+ 225 232 236 238 239 248 267 271 293 297 299 302 318 326 340 341 359 368
7
+ 374 380 386 387 388 393 406 410 416 424 427]
8
+
9
+ Confusion Matrix:
10
+ [[280 0]
11
+ [ 84 65]]
12
+ TN: 280, FP: 0
13
+ FN: 84, TP: 65
14
+
15
+ === Metrics for Anomaly Detection ===
16
+ Accuracy : 0.8042
17
+ Precision: 1.0000
18
+ Recall : 0.4362
19
+ F1-score : 0.6075
20
+
21
+ === Classification Report ===
22
+ precision recall f1-score support
23
+
24
+ Normal (0) 0.77 1.00 0.87 280
25
+ Anomaly (1) 1.00 0.44 0.61 149
26
+
27
+ accuracy 0.80 429
28
+ macro avg 0.88 0.72 0.74 429
29
+ weighted avg 0.85 0.80 0.78 429
Report/vae_results.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Threshold (mean + 2*std): 0.040195
2
+ Detected 75 anomalies out of 429 samples
3
+ Anomaly indices: [ 7 8 10 15 23 27 38 49 56 58 59 61 67 71 72 79 92 98
4
+ 102 107 121 124 125 128 132 138 149 150 155 156 157 162 170 189 191 195
5
+ 200 207 208 210 214 215 216 221 225 235 236 238 239 248 250 260 261 267
6
+ 271 279 293 297 299 302 318 323 326 340 359 368 374 386 387 388 394 406
7
+ 409 410 427]
8
+
9
+ Confusion Matrix:
10
+ [[279 1]
11
+ [ 75 74]]
12
+ TN: 279, FP: 1
13
+ FN: 75, TP: 74
14
+
15
+ === Metrics ===
16
+ Accuracy : 0.8228
17
+ Precision: 0.9867
18
+ Recall : 0.4966
19
+ F1-score : 0.6607
20
+
21
+ Classification Report:
22
+ precision recall f1-score support
23
+
24
+ Normal (0) 0.79 1.00 0.88 280
25
+ Anomaly (1) 0.99 0.50 0.66 149
26
+
27
+ accuracy 0.82 429
28
+ macro avg 0.89 0.75 0.77 429
29
+ weighted avg 0.86 0.82 0.80 429
Transformer‐AE.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import numpy as np
4
+ import pandas as pd
5
+ import torch
6
+ import torch.nn as nn
7
+ from torch.utils.data import Dataset, DataLoader, Subset
8
+ from sklearn.preprocessing import MinMaxScaler
9
+ from sklearn.model_selection import StratifiedShuffleSplit
10
+ from sklearn.metrics import (
11
+ confusion_matrix,
12
+ precision_score,
13
+ recall_score,
14
+ f1_score,
15
+ accuracy_score,
16
+ classification_report
17
+ )
18
+
19
+ # ============================================
20
+ # 1. CÀI ĐẶT THAM SỐ CHUNG
21
+ # ============================================
22
+ DATA_PATH = "dataset.xlsx"
23
+
24
+ FEATURE_COLUMNS = [
25
+ "Temp",
26
+ "Turbidity (cm)",
27
+ "DO(mg/L)",
28
+ "BOD (mg/L)",
29
+ "CO2",
30
+ "pH`",
31
+ "Alkalinity (mg L-1 )",
32
+ "Hardness (mg L-1 )",
33
+ "Calcium (mg L-1 )",
34
+ "Ammonia (mg L-1 )",
35
+ "Nitrite (mg L-1 )",
36
+ "Phosphorus (mg L-1 )",
37
+ "H2S (mg L-1 )",
38
+ "Plankton (No. L-1)"
39
+ ]
40
+ LABEL_COL = "Water Quality"
41
+
42
+ SEQUENCE_LENGTH = 10
43
+ TRAIN_RATIO = 0.8
44
+ VAL_RATIO = 0.1
45
+ TEST_RATIO = 0.1
46
+
47
+ # Transformer AE hyperparameters
48
+ D_MODEL = 64 # embedding dimension
49
+ NHEAD = 8 # number of attention heads
50
+ NUM_LAYERS = 3 # number of transformer layers
51
+ DIM_FEEDFORWARD = 128 # feedforward network dimension
52
+ AE_LR = 1e-3
53
+ AE_EPOCHS = 50
54
+ BATCH_SIZE = 64
55
+ RANDOM_STATE = 42
56
+
57
+ THRESHOLD_STD_FACTOR = 2 # threshold = mean + 2*std
58
+
59
+ # ============================================
60
+ # 2. ĐỌC VÀ TIỀN XỬ LÝ DỮ LIỆU
61
+ # ============================================
62
+ df = pd.read_excel(DATA_PATH)
63
+ df = df.dropna(how="all")
64
+ df[LABEL_COL] = df[LABEL_COL].astype(int)
65
+ for col in FEATURE_COLUMNS:
66
+ if df[col].dtype == object:
67
+ df[col] = df[col].str.replace(",", ".")
68
+ df[col] = df[col].astype(float)
69
+
70
+ data_raw = df[FEATURE_COLUMNS].values
71
+ scaler = MinMaxScaler()
72
+ data_scaled = scaler.fit_transform(data_raw)
73
+ labels_all = df[LABEL_COL].values
74
+
75
+ # ============================================
76
+ # 3. DATASET CHO TIME-SERIES
77
+ # ============================================
78
+ class TimeSeriesDataset(Dataset):
79
+ def __init__(self, data, seq_len):
80
+ self.data = data
81
+ self.seq_len = seq_len
82
+
83
+ def __len__(self):
84
+ return self.data.shape[0] - self.seq_len
85
+
86
+ def __getitem__(self, idx):
87
+ seq = self.data[idx : idx + self.seq_len] # (seq_len, features)
88
+ return torch.tensor(seq, dtype=torch.float32)
89
+
90
+ num_items = data_scaled.shape[0] - SEQUENCE_LENGTH
91
+ y_seq = np.array([labels_all[i + SEQUENCE_LENGTH] for i in range(num_items)], dtype=int)
92
+
93
+ sss1 = StratifiedShuffleSplit(n_splits=1, test_size=TEST_RATIO, random_state=RANDOM_STATE)
94
+ for train_val_idx, test_idx in sss1.split(np.zeros(num_items), y_seq): pass
95
+ val_rel = VAL_RATIO / (TRAIN_RATIO + VAL_RATIO)
96
+ sss2 = StratifiedShuffleSplit(n_splits=1, test_size=val_rel, random_state=RANDOM_STATE)
97
+ for ti, vi in sss2.split(np.zeros(len(train_val_idx)), y_seq[train_val_idx]): pass
98
+ train_idx = train_val_idx[ti]; val_idx = train_val_idx[vi]
99
+
100
+ dataset = TimeSeriesDataset(data_scaled, SEQUENCE_LENGTH)
101
+ train_normal = [i for i in train_idx if y_seq[i] < 2]
102
+ val_normal = [i for i in val_idx if y_seq[i] < 2]
103
+
104
+ train_loader = DataLoader(Subset(dataset, train_normal), batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
105
+ val_loader = DataLoader(Subset(dataset, val_normal), batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
106
+ test_loader = DataLoader(Subset(dataset, test_idx), batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
107
+
108
+ # ============================================
109
+ # 4. POSITIONAL ENCODING
110
+ # ============================================
111
+ class PositionalEncoding(nn.Module):
112
+ def __init__(self, d_model, dropout=0.1, max_len=5000):
113
+ super().__init__()
114
+ self.dropout = nn.Dropout(p=dropout)
115
+ pe = torch.zeros(max_len, d_model)
116
+ pos = torch.arange(max_len, dtype=torch.float).unsqueeze(1)
117
+ div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0)/d_model))
118
+ pe[:,0::2] = torch.sin(pos * div)
119
+ pe[:,1::2] = torch.cos(pos * div)
120
+ self.register_buffer('pe', pe.unsqueeze(1))
121
+
122
+ def forward(self, x):
123
+ # x: (seq_len, batch, d_model)
124
+ return self.dropout(x + self.pe[:x.size(0)])
125
+
126
+ # ============================================
127
+ # 5. TRANSFORMER AUTOENCODER
128
+ # ============================================
129
+ class TransformerAutoencoder(nn.Module):
130
+ def __init__(self, feat_dim, seq_len, d_model, nhead, nlayers, ff_dim, dropout=0.1):
131
+ super().__init__()
132
+ self.d_model = d_model
133
+ self.input_proj = nn.Linear(feat_dim, d_model)
134
+ self.pos_enc = PositionalEncoding(d_model, dropout)
135
+ enc_layer = nn.TransformerEncoderLayer(d_model, nhead, ff_dim, dropout)
136
+ self.encoder = nn.TransformerEncoder(enc_layer, nlayers)
137
+ dec_layer = nn.TransformerDecoderLayer(d_model, nhead, ff_dim, dropout)
138
+ self.decoder = nn.TransformerDecoder(dec_layer, nlayers)
139
+ self.output_proj = nn.Linear(d_model, feat_dim)
140
+ mask = torch.triu(torch.ones(seq_len, seq_len)*float('-inf'), diagonal=1)
141
+ self.register_buffer('mask', mask)
142
+
143
+ def forward(self, src):
144
+ # src: (batch, seq_len, feat_dim)
145
+ x = self.input_proj(src) * math.sqrt(self.d_model)
146
+ x = x.permute(1,0,2) # (seq_len, batch, d_model)
147
+ x = self.pos_enc(x)
148
+ memory = self.encoder(x)
149
+ tgt = torch.zeros_like(x)
150
+ out = self.decoder(tgt, memory, tgt_mask=self.mask)
151
+ out = out.permute(1,0,2) # (batch, seq_len, d_model)
152
+ return self.output_proj(out) # (batch, seq_len, feat_dim)
153
+
154
+ # ============================================
155
+ # 6. KHỞI TẠO MÔ HÌNH VÀ OPTIMIZER
156
+ # ============================================
157
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
158
+ model = TransformerAutoencoder(
159
+ feat_dim=len(FEATURE_COLUMNS),
160
+ seq_len=SEQUENCE_LENGTH,
161
+ d_model=D_MODEL,
162
+ nhead=NHEAD,
163
+ nlayers=NUM_LAYERS,
164
+ ff_dim=DIM_FEEDFORWARD
165
+ ).to(device)
166
+ criterion = nn.MSELoss()
167
+ optimizer = torch.optim.Adam(model.parameters(), lr=AE_LR)
168
+
169
+ # ============================================
170
+ # 7. TRAINING
171
+ # ============================================
172
+ best_val = float('inf')
173
+ best_path = 'best_transformer_ae.pth'
174
+ for ep in range(1, AE_EPOCHS+1):
175
+ model.train()
176
+ tr_loss = 0
177
+ for b in train_loader:
178
+ b = b.to(device)
179
+ optimizer.zero_grad()
180
+ recon = model(b)
181
+ loss = criterion(recon, b)
182
+ loss.backward()
183
+ optimizer.step()
184
+ tr_loss += loss.item()*b.size(0)
185
+ tr_loss /= len(train_loader.dataset)
186
+
187
+ model.eval()
188
+ vl_loss = 0
189
+ with torch.no_grad():
190
+ for b in val_loader:
191
+ b = b.to(device)
192
+ recon = model(b)
193
+ vl_loss += criterion(recon, b).item()*b.size(0)
194
+ vl_loss /= len(val_loader.dataset)
195
+
196
+ print(f"Epoch {ep:02d} | Train Loss: {tr_loss:.6f} | Val Loss: {vl_loss:.6f}")
197
+ if vl_loss < best_val:
198
+ best_val = vl_loss
199
+ torch.save(model.state_dict(), best_path)
200
+
201
+ model.load_state_dict(torch.load(best_path, map_location=device))
202
+
203
+ # ============================================
204
+ # 8. ĐÁNH GIÁ TEST & XÁC ĐỊNH THRESHOLD
205
+ # ============================================
206
+ model.eval()
207
+ # Test Loss
208
+ test_losses = []
209
+ with torch.no_grad():
210
+ for b in test_loader:
211
+ b = b.to(device)
212
+ recon = model(b)
213
+ test_losses.append(criterion(recon, b).item()*b.size(0))
214
+ test_loss = sum(test_losses)/len(test_loader.dataset)
215
+ print(f"Test Loss: {test_loss:.6f}")
216
+
217
+ # Reconstruction error on validation normal
218
+ val_errs = np.concatenate([
219
+ torch.mean((model(b.to(device))-b.to(device))**2, dim=(1,2)).detach().cpu().numpy()
220
+ for b in val_loader
221
+ ])
222
+ mu, sd = val_errs.mean(), val_errs.std()
223
+ threshold = mu + THRESHOLD_STD_FACTOR*sd
224
+ print(f"Threshold (mean + 2*std): {threshold:.6f}")
225
+
226
+ # ============================================
227
+ # 9. PHÁT HIỆN BẤT THƯỜNG TRÊN TEST
228
+ # ============================================
229
+ test_errs = np.concatenate([
230
+ torch.mean((model(b.to(device))-b.to(device))**2, dim=(1,2)).detach().cpu().numpy()
231
+ for b in test_loader
232
+ ])
233
+ anom = test_errs > threshold
234
+ num_anom = anom.sum()
235
+ print(f"Phát hiện {num_anom} samples bất thường trong tập test (trên tổng {len(test_errs)})")
236
+ print(f"Chỉ số sample bất thường: {np.where(anom)[0]}")
237
+
238
+ # ============================================
239
+ # 10. ĐÁNH GIÁ KẾT QUẢ
240
+ # ============================================
241
+ y_true = np.array([1 if labels_all[i+SEQUENCE_LENGTH]==2 else 0 for i in test_idx], dtype=int)
242
+ y_pred = anom.astype(int)
243
+
244
+ cm = confusion_matrix(y_true, y_pred)
245
+ tn, fp, fn, tp = cm.ravel()
246
+ acc = accuracy_score(y_true, y_pred)
247
+ prec = precision_score(y_true, y_pred, zero_division=0)
248
+ rec = recall_score(y_true, y_pred, zero_division=0)
249
+ f1 = f1_score(y_true, y_pred, zero_division=0)
250
+
251
+ print("\n=== Confusion Matrix ===")
252
+ print(cm)
253
+ print(f"TN: {tn}, FP: {fp}")
254
+ print(f"FN: {fn}, TP: {tp}\n")
255
+
256
+ print("=== Metrics for Anomaly Detection ===")
257
+ print(f"Accuracy : {acc:.4f}")
258
+ print(f"Precision: {prec:.4f}")
259
+ print(f"Recall : {rec:.4f}")
260
+ print(f"F1-score : {f1:.4f}\n")
261
+
262
+ print("=== Classification Report ===")
263
+ report = classification_report(y_true, y_pred, target_names=["Normal (0)", "Anomaly (1)"], zero_division=0)
264
+ print(report)
265
+
266
+ # ============================================
267
+ # 11. GHI RA FILE TXT
268
+ # ============================================
269
+ out = []
270
+ out.append(f"Test Loss: {test_loss:.6f}")
271
+ out.append(f"Threshold (mean + 2*std): {threshold:.6f}")
272
+ out.append(f"Phát hiện {num_anom} samples bất thường trên tổng {len(test_errs)}")
273
+ out.append(f"Chỉ số sample bất thường: {np.where(anom)[0]}")
274
+ out.append("")
275
+ out.append("Confusion Matrix:")
276
+ out.append(np.array2string(cm))
277
+ out.append(f"TN: {tn}, FP: {fp}")
278
+ out.append(f"FN: {fn}, TP: {tp}")
279
+ out.append("")
280
+ out.append("=== Metrics for Anomaly Detection ===")
281
+ out.append(f"Accuracy : {acc:.4f}")
282
+ out.append(f"Precision: {prec:.4f}")
283
+ out.append(f"Recall : {rec:.4f}")
284
+ out.append(f"F1-score : {f1:.4f}")
285
+ out.append("")
286
+ out.append("=== Classification Report ===")
287
+ out.append(report)
288
+
289
+ with open("transformer_ae_results.txt", "w") as f:
290
+ f.write("\n".join(out))
291
+
292
+ print("Results saved to transformer_ae_results.txt")
VAE.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import numpy as np
4
+ import pandas as pd
5
+ import torch
6
+ import torch.nn as nn
7
+ from torch.utils.data import Dataset, DataLoader, Subset
8
+ from sklearn.preprocessing import MinMaxScaler
9
+ from sklearn.model_selection import StratifiedShuffleSplit
10
+ from sklearn.metrics import (
11
+ confusion_matrix,
12
+ precision_score,
13
+ recall_score,
14
+ f1_score,
15
+ accuracy_score,
16
+ classification_report
17
+ )
18
+
19
+ # ============================================
20
+ # 1. CÀI ĐẶT THAM SỐ CHUNG
21
+ # ============================================
22
+ DATA_PATH = "dataset.xlsx"
23
+
24
+ FEATURE_COLUMNS = [
25
+ "Temp", "Turbidity (cm)", "DO(mg/L)", "BOD (mg/L)", "CO2", "pH`",
26
+ "Alkalinity (mg L-1 )", "Hardness (mg L-1 )", "Calcium (mg L-1 )",
27
+ "Ammonia (mg L-1 )", "Nitrite (mg L-1 )", "Phosphorus (mg L-1 )",
28
+ "H2S (mg L-1 )", "Plankton (No. L-1)"
29
+ ]
30
+ LABEL_COL = "Water Quality"
31
+
32
+ SEQUENCE_LENGTH = 10
33
+ TRAIN_RATIO = 0.8
34
+ VAL_RATIO = 0.1
35
+ TEST_RATIO = 0.1
36
+
37
+ # VAE hyperparameters
38
+ LATENT_DIM = 16
39
+ HIDDEN_DIM = 128
40
+ AE_LR = 1e-3
41
+ AE_EPOCHS = 50
42
+ BATCH_SIZE = 64
43
+ RANDOM_STATE = 42
44
+
45
+ THRESHOLD_STD_FACTOR = 2 # threshold = mean + 2*std
46
+
47
+ # ============================================
48
+ # 2. ĐỌC VÀ TIỀN XỬ LÝ DỮ LIỆU
49
+ # ============================================
50
+ df = pd.read_excel(DATA_PATH)
51
+ df = df.dropna(how="all")
52
+ df[LABEL_COL] = df[LABEL_COL].astype(int)
53
+ for col in FEATURE_COLUMNS:
54
+ if df[col].dtype == object:
55
+ df[col] = df[col].str.replace(",", ".")
56
+ df[col] = df[col].astype(float)
57
+
58
+ data_raw = df[FEATURE_COLUMNS].values
59
+ scaler = MinMaxScaler()
60
+ data_scaled = scaler.fit_transform(data_raw)
61
+ labels_all = df[LABEL_COL].values
62
+
63
+ # ============================================
64
+ # 3. DATASET CHO TIME-SERIES
65
+ # ============================================
66
+ class TimeSeriesDataset(Dataset):
67
+ def __init__(self, data, seq_len):
68
+ self.data = data
69
+ self.seq_len = seq_len
70
+
71
+ def __len__(self):
72
+ return self.data.shape[0] - self.seq_len
73
+
74
+ def __getitem__(self, idx):
75
+ seq = self.data[idx:idx+self.seq_len] # (seq_len, features)
76
+ return torch.tensor(seq, dtype=torch.float32)
77
+
78
+ num_items = data_scaled.shape[0] - SEQUENCE_LENGTH
79
+ y_seq = np.array([labels_all[i+SEQUENCE_LENGTH] for i in range(num_items)], dtype=int)
80
+
81
+ # Stratified split
82
+ sss1 = StratifiedShuffleSplit(n_splits=1, test_size=TEST_RATIO, random_state=RANDOM_STATE)
83
+ for tv_idx, test_idx in sss1.split(np.zeros(num_items), y_seq): pass
84
+ val_rel = VAL_RATIO / (TRAIN_RATIO + VAL_RATIO)
85
+ sss2 = StratifiedShuffleSplit(n_splits=1, test_size=val_rel, random_state=RANDOM_STATE)
86
+ for tr_rel, val_rel_idx in sss2.split(np.zeros(len(tv_idx)), y_seq[tv_idx]): pass
87
+
88
+ train_idx = tv_idx[tr_rel]
89
+ val_idx = tv_idx[val_rel_idx]
90
+
91
+ dataset = TimeSeriesDataset(data_scaled, SEQUENCE_LENGTH)
92
+ train_norm = [i for i in train_idx if y_seq[i] < 2]
93
+ val_norm = [i for i in val_idx if y_seq[i] < 2]
94
+
95
+ train_loader = DataLoader(Subset(dataset, train_norm), batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
96
+ val_loader = DataLoader(Subset(dataset, val_norm), batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
97
+ test_loader = DataLoader(Subset(dataset, test_idx), batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
98
+
99
+ # ============================================
100
+ # 4. ĐỊNH NGHĨA VAE
101
+ # ============================================
102
+ class VAE(nn.Module):
103
+ def __init__(self, input_dim, seq_len, hidden_dim, latent_dim):
104
+ super().__init__()
105
+ self.seq_len = seq_len
106
+ self.input_dim = input_dim
107
+ flat_dim = seq_len * input_dim
108
+
109
+ # encoder
110
+ self.fc1 = nn.Linear(flat_dim, hidden_dim)
111
+ self.fc_mu = nn.Linear(hidden_dim, latent_dim)
112
+ self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
113
+
114
+ # decoder
115
+ self.fc3 = nn.Linear(latent_dim, hidden_dim)
116
+ self.fc4 = nn.Linear(hidden_dim, flat_dim)
117
+ self.act = nn.ReLU()
118
+ self.sig = nn.Sigmoid()
119
+
120
+ def encode(self, x):
121
+ h1 = self.act(self.fc1(x))
122
+ return self.fc_mu(h1), self.fc_logvar(h1)
123
+
124
+ def reparameterize(self, mu, logvar):
125
+ std = torch.exp(0.5 * logvar)
126
+ eps = torch.randn_like(std)
127
+ return mu + eps * std
128
+
129
+ def decode(self, z):
130
+ h3 = self.act(self.fc3(z))
131
+ return self.sig(self.fc4(h3))
132
+
133
+ def forward(self, x):
134
+ # x: (batch, seq_len, input_dim)
135
+ x_flat = x.view(x.size(0), -1)
136
+ mu, logvar = self.encode(x_flat)
137
+ z = self.reparameterize(mu, logvar)
138
+ recon_flat = self.decode(z)
139
+ recon = recon_flat.view(x.size(0), self.seq_len, self.input_dim)
140
+ return recon, mu, logvar
141
+
142
+ # ============================================
143
+ # 5. KHỞI TẠO MÔ HÌNH, OPTIMIZER
144
+ # ============================================
145
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
146
+ model = VAE(len(FEATURE_COLUMNS), SEQUENCE_LENGTH, HIDDEN_DIM, LATENT_DIM).to(device)
147
+ optimizer = torch.optim.Adam(model.parameters(), lr=AE_LR)
148
+ mse_loss = nn.MSELoss(reduction='sum')
149
+
150
+ # KL divergence term
151
+ def kl_divergence(mu, logvar):
152
+ return -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
153
+
154
+ # ============================================
155
+ # 6. HUẤN LUYỆN VAE
156
+ # ============================================
157
+ best_val_loss = float('inf')
158
+ best_path = 'best_vae.pth'
159
+
160
+ for epoch in range(1, AE_EPOCHS+1):
161
+ model.train()
162
+ train_loss = 0
163
+ for batch in train_loader:
164
+ batch = batch.to(device)
165
+ recon, mu, logvar = model(batch)
166
+ recon_loss = mse_loss(recon, batch)
167
+ kl_loss = kl_divergence(mu, logvar)
168
+ loss = recon_loss + kl_loss
169
+ optimizer.zero_grad()
170
+ loss.backward()
171
+ optimizer.step()
172
+ train_loss += loss.item()
173
+ train_loss /= len(train_loader.dataset)
174
+
175
+ model.eval()
176
+ val_loss = 0
177
+ with torch.no_grad():
178
+ for batch in val_loader:
179
+ batch = batch.to(device)
180
+ recon, mu, logvar = model(batch)
181
+ recon_loss = mse_loss(recon, batch)
182
+ kl_loss = kl_divergence(mu, logvar)
183
+ val_loss += (recon_loss + kl_loss).item()
184
+ val_loss /= len(val_loader.dataset)
185
+
186
+ print(f"Epoch {epoch:02d} | Train Loss: {train_loss:.6f} | Val Loss: {val_loss:.6f}")
187
+ if val_loss < best_val_loss:
188
+ best_val_loss = val_loss
189
+ torch.save(model.state_dict(), best_path)
190
+
191
+ model.load_state_dict(torch.load(best_path, map_location=device))
192
+
193
+ # ============================================
194
+ # 7. XÁC ĐỊNH NGƯỠNG RECONSTRUCTION ERROR
195
+ # ============================================
196
+ model.eval()
197
+ val_errors = []
198
+ with torch.no_grad():
199
+ for batch in val_loader:
200
+ batch = batch.to(device)
201
+ recon, _, _ = model(batch)
202
+ # compute per-sample MSE
203
+ errs = torch.mean((recon - batch)**2, dim=(1,2))
204
+ val_errors.append(errs.cpu().numpy())
205
+ val_errors = np.concatenate(val_errors)
206
+ mu_err, sigma_err = val_errors.mean(), val_errors.std()
207
+ threshold = mu_err + THRESHOLD_STD_FACTOR * sigma_err
208
+ print(f"Threshold (mean + 2*std): {threshold:.6f}")
209
+
210
+ # ============================================
211
+ # 8. PHÁT HIỆN BẤT THƯỜNG TRÊN TEST
212
+ # ============================================
213
+ test_errors = []
214
+ with torch.no_grad():
215
+ for batch in test_loader:
216
+ batch = batch.to(device)
217
+ recon, _, _ = model(batch)
218
+ errs = torch.mean((recon - batch)**2, dim=(1,2))
219
+ test_errors.append(errs.cpu().numpy())
220
+ test_errors = np.concatenate(test_errors)
221
+ anomalies = test_errors > threshold
222
+ print(f"Detected {anomalies.sum()} anomalies out of {len(test_errors)} samples")
223
+ print("Anomaly indices:", np.where(anomalies)[0])
224
+
225
+ # ============================================
226
+ # 9. ĐÁNH GIÁ KẾT QUẢ
227
+ # ============================================
228
+ y_true = np.array([1 if labels_all[i+SEQUENCE_LENGTH]==2 else 0 for i in test_idx], dtype=int)
229
+ y_pred = anomalies.astype(int)
230
+
231
+ cm = confusion_matrix(y_true, y_pred)
232
+ tn, fp, fn, tp = cm.ravel()
233
+ accuracy = accuracy_score(y_true, y_pred)
234
+ precision = precision_score(y_true, y_pred, zero_division=0)
235
+ recall = recall_score(y_true, y_pred, zero_division=0)
236
+ f1 = f1_score(y_true, y_pred, zero_division=0)
237
+
238
+ print("\n=== Confusion Matrix ===")
239
+ print(cm)
240
+ print(f"TN: {tn}, FP: {fp}")
241
+ print(f"FN: {fn}, TP: {tp}\n")
242
+
243
+ print("=== Metrics for Anomaly Detection ===")
244
+ print(f"Accuracy : {accuracy:.4f}")
245
+ print(f"Precision: {precision:.4f}")
246
+ print(f"Recall : {recall:.4f}")
247
+ print(f"F1-score : {f1:.4f}\n")
248
+
249
+ print("=== Classification Report ===")
250
+ print(classification_report(y_true, y_pred, target_names=["Normal (0)", "Anomaly (1)"], zero_division=0))
251
+
252
+ # ============================================
253
+ # 10. GHI KẾT QUẢ RA FILE TXT
254
+ # ============================================
255
+ results = []
256
+ results.append(f"Threshold (mean + 2*std): {threshold:.6f}")
257
+ results.append(f"Detected {anomalies.sum()} anomalies out of {len(test_errors)} samples")
258
+ results.append("Anomaly indices: " + np.array2string(np.where(anomalies)[0]))
259
+ results.append("\nConfusion Matrix:\n" + np.array2string(cm))
260
+ results.append(f"TN: {tn}, FP: {fp}")
261
+ results.append(f"FN: {fn}, TP: {tp}")
262
+ results.append("\n=== Metrics ===")
263
+ results.append(f"Accuracy : {accuracy:.4f}")
264
+ results.append(f"Precision: {precision:.4f}")
265
+ results.append(f"Recall : {recall:.4f}")
266
+ results.append(f"F1-score : {f1:.4f}")
267
+ results.append("\nClassification Report:\n" + classification_report(y_true, y_pred, target_names=["Normal (0)", "Anomaly (1)"], zero_division=0))
268
+
269
+ with open("vae_results.txt", "w") as f:
270
+ f.write("\n".join(results))
271
+
272
+ print("Results saved to vae_results.txt")
best_cnn_ae.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:71d27e809c30c0ab42fc9a44ad99673f893133147e6458a31abd7a09e53fb50f
3
+ size 262450
best_lstm_ae.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:07519d59d401f056235216106ea23e6b9d907264c011e01beebcb7e832a504f0
3
+ size 488376
best_lstm_forecaster.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f21c5ade7be5c3e426c263bb38e6be71a655cdb60ce7babfebf6c3c432cd29d5
3
+ size 221231
best_sequence_autoencoder.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:57187c839f3e00acd0951887abe0f8032524427bac56213b098d2d82b9816a9a
3
+ size 109464
best_transformer_ae.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dbad40214173db46fa2b8f9af7c04fc1133f82c532749daa3ea4c80670de2d47
3
+ size 2328294
best_vae.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dec19da3ece3538ef08aba042726b6895515e7ec72cb3fd002847add0e9937ff
3
+ size 173446
main_Fully‐Connected-Sequence-Autoencoder.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.preprocessing import MinMaxScaler
5
+ from sklearn.model_selection import StratifiedShuffleSplit
6
+ from sklearn.metrics import (
7
+ confusion_matrix,
8
+ precision_score,
9
+ recall_score,
10
+ f1_score,
11
+ accuracy_score,
12
+ classification_report
13
+ )
14
+ import torch
15
+ import torch.nn as nn
16
+ from torch.utils.data import Dataset, DataLoader, Subset
17
+
18
+ # ============================================
19
+ # 1. CÀI ĐẶT CÁC THAM SỐ CHUNG
20
+ # ============================================
21
+
22
+ # Đường dẫn tới file Excel
23
+ DATA_PATH = "dataset.xlsx"
24
+
25
+ # Tên các cột đặc trưng (không bao gồm cột label "Water Quality")
26
+ FEATURE_COLUMNS = [
27
+ "Temp",
28
+ "Turbidity (cm)",
29
+ "DO(mg/L)",
30
+ "BOD (mg/L)",
31
+ "CO2",
32
+ "pH`",
33
+ "Alkalinity (mg L-1 )",
34
+ "Hardness (mg L-1 )",
35
+ "Calcium (mg L-1 )",
36
+ "Ammonia (mg L-1 )",
37
+ "Nitrite (mg L-1 )",
38
+ "Phosphorus (mg L-1 )",
39
+ "H2S (mg L-1 )",
40
+ "Plankton (No. L-1)"
41
+ ]
42
+ LABEL_COL = "Water Quality"
43
+
44
+ # Độ dài sequence (sliding window)
45
+ SEQUENCE_LENGTH = 10
46
+
47
+ # Tỉ lệ chia train / validation / test
48
+ TRAIN_RATIO = 0.8
49
+ VAL_RATIO = 0.1
50
+ TEST_RATIO = 0.1
51
+
52
+ # Hyper-parameters cho Autoencoder
53
+ INPUT_SIZE = len(FEATURE_COLUMNS) # 14
54
+ SEQ_LEN = SEQUENCE_LENGTH # 10
55
+ FLAT_DIM = SEQ_LEN * INPUT_SIZE # 10 * 14 = 140
56
+ LATENT_DIM = 64
57
+ AE_HIDDEN_DIM = 64
58
+
59
+ BATCH_SIZE = 64
60
+ AE_LR = 1e-3
61
+ AE_EPOCHS = 50
62
+ RANDOM_STATE = 42
63
+
64
+ # Hyper-parameters cho anomaly threshold
65
+ THRESHOLD_STD_FACTOR = 2 # mean + 2 * std
66
+
67
+ # ============================================
68
+ # 2. ĐỌC VÀ TIỀN XỬ LÝ DỮ LIỆU
69
+ # ============================================
70
+
71
+ # 2.1 Đọc toàn bộ DataFrame, loại bỏ các hàng hoàn toàn rỗng
72
+ df = pd.read_excel(DATA_PATH)
73
+ df = df.dropna(how="all")
74
+
75
+ # 2.2 Chuyển cột label sang int
76
+ df[LABEL_COL] = df[LABEL_COL].astype(int)
77
+ labels_all = df[LABEL_COL].values # mảng nhãn gốc, shape = (num_total,)
78
+
79
+ # 2.3 Chuyển đổi các cột đặc trưng: thay dấu phẩy thành dấu chấm, convert sang float
80
+ for col in FEATURE_COLUMNS:
81
+ if df[col].dtype == object or df[col].dtype == str:
82
+ df[col] = df[col].apply(lambda x: str(x).replace(",", "."))
83
+ df[col] = df[col].astype(float)
84
+
85
+ # 2.4 Lấy mảng dữ liệu số (chỉ gồm các cột FEATURE_COLUMNS)
86
+ data_raw = df[FEATURE_COLUMNS].values # shape = (num_total, 14)
87
+
88
+ # 2.5 Chuẩn hóa Min-Max
89
+ scaler = MinMaxScaler()
90
+ data_scaled = scaler.fit_transform(data_raw) # shape = (num_total, 14)
91
+
92
+ # ============================================
93
+ # 3. TẠO CLASS Dataset CHO TIME-SERIES
94
+ # ============================================
95
+
96
+ class TimeSeriesDataset(Dataset):
97
+ """
98
+ Dataset trả về:
99
+ - x: một cửa sổ (sequence) có shape (sequence_length, num_features)
100
+ - y: vector giá trị next step có shape (num_features,)
101
+ """
102
+ def __init__(self, data, seq_len):
103
+ self.data = data
104
+ self.seq_len = seq_len
105
+ self.num_items = data.shape[0] - seq_len
106
+
107
+ def __len__(self):
108
+ return self.num_items
109
+
110
+ def __getitem__(self, idx):
111
+ """
112
+ Trả về:
113
+ x_seq: shape (seq_len, num_features)
114
+ y_next: shape (num_features,)
115
+ """
116
+ x_seq = self.data[idx : idx + self.seq_len]
117
+ y_next = self.data[idx + self.seq_len]
118
+ return (
119
+ torch.tensor(x_seq, dtype=torch.float32),
120
+ torch.tensor(y_next, dtype=torch.float32)
121
+ )
122
+
123
+ # ============================================
124
+ # 4. TẠO MẢNG NHÃN CHO MỖI SEQUENCE (DỰA TRÊN Water Quality)
125
+ # ============================================
126
+
127
+ num_total = data_scaled.shape[0]
128
+ num_items = num_total - SEQUENCE_LENGTH # số lượng sequence-to-one
129
+
130
+ y_seq = np.zeros(num_items, dtype=int)
131
+ for i in range(num_items):
132
+ idx_label = i + SEQUENCE_LENGTH
133
+ y_seq[i] = labels_all[idx_label] # 0, 1, hoặc 2
134
+
135
+ # ============================================
136
+ # 5. CHIA DỮ LIỆU CÓ PHÂN LỚP (STRATIFIED SPLIT) THEO y_seq
137
+ # ============================================
138
+
139
+ # 5.1 Chia test (10%)
140
+ sss1 = StratifiedShuffleSplit(n_splits=1, test_size=TEST_RATIO, random_state=RANDOM_STATE)
141
+ for train_val_idx, test_idx in sss1.split(np.zeros(num_items), y_seq):
142
+ pass
143
+
144
+ # 5.2 Từ train_val (90%), chia tiếp train (80%) và val (10%)
145
+ train_val_labels = y_seq[train_val_idx]
146
+ val_size_relative = VAL_RATIO / (TRAIN_RATIO + VAL_RATIO)
147
+ sss2 = StratifiedShuffleSplit(n_splits=1, test_size=val_size_relative, random_state=RANDOM_STATE)
148
+ for train_idx_rel, val_idx_rel in sss2.split(np.zeros(len(train_val_idx)), train_val_labels):
149
+ pass
150
+
151
+ train_idx = train_val_idx[train_idx_rel]
152
+ val_idx = train_val_idx[val_idx_rel]
153
+
154
+ # Hàm đếm phân bố nhãn
155
+ def count_labels(indices, y):
156
+ unique, counts = np.unique(y[indices], return_counts=True)
157
+ return dict(zip(unique.tolist(), counts.tolist()))
158
+
159
+ print("Phân bố nh��n trong tập train:", count_labels(train_idx, y_seq))
160
+ print("Phân bố nhãn trong tập val :", count_labels(val_idx, y_seq))
161
+ print("Phân bố nhãn trong tập test :", count_labels(test_idx, y_seq))
162
+
163
+ # ============================================
164
+ # 6. TẠO DATALOADER CHO Train/Val/Test (cho reconstruction)
165
+ # ============================================
166
+
167
+ # Tạo dataset tổng
168
+ dataset_all = TimeSeriesDataset(data_scaled, SEQUENCE_LENGTH)
169
+
170
+ # Lọc chỉ các sequence “normal” (nhãn 0 hoặc 1) để huấn luyện autoencoder
171
+ train_normal_idx = [i for i in train_idx if y_seq[i] < 2]
172
+ val_normal_idx = [i for i in val_idx if y_seq[i] < 2]
173
+
174
+ train_ae_dataset = Subset(dataset_all, train_normal_idx)
175
+ val_ae_dataset = Subset(dataset_all, val_normal_idx)
176
+ test_dataset = Subset(dataset_all, test_idx) # Dùng toàn bộ test để đánh giá sau
177
+
178
+ train_ae_loader = DataLoader(train_ae_dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
179
+ val_ae_loader = DataLoader(val_ae_dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
180
+ test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
181
+
182
+ # ============================================
183
+ # 7. XÂY DỰNG MÔ HÌNH AUTOENCODER (FC)
184
+ # ============================================
185
+
186
+ class SequenceAutoencoder(nn.Module):
187
+ def __init__(self, seq_len, input_dim, latent_dim):
188
+ super(SequenceAutoencoder, self).__init__()
189
+ self.seq_len = seq_len
190
+ self.input_dim = input_dim
191
+ self.flat_dim = seq_len * input_dim
192
+
193
+ # Encoder: 140 -> LATENT_DIM
194
+ self.encoder = nn.Sequential(
195
+ nn.Linear(self.flat_dim, AE_HIDDEN_DIM),
196
+ nn.ReLU(),
197
+ nn.Linear(AE_HIDDEN_DIM, latent_dim),
198
+ nn.ReLU()
199
+ )
200
+ # Decoder: LATENT_DIM -> 140
201
+ self.decoder = nn.Sequential(
202
+ nn.Linear(latent_dim, AE_HIDDEN_DIM),
203
+ nn.ReLU(),
204
+ nn.Linear(AE_HIDDEN_DIM, self.flat_dim),
205
+ )
206
+
207
+ def forward(self, x):
208
+ """
209
+ x: shape (batch, seq_len, input_dim)
210
+ Trả về x_recon: shape (batch, seq_len, input_dim)
211
+ """
212
+ batch_size = x.size(0)
213
+ # Flatten: (batch, seq_len * input_dim)
214
+ x_flat = x.view(batch_size, -1)
215
+ z = self.encoder(x_flat) # (batch, latent_dim)
216
+ out_flat = self.decoder(z) # (batch, flat_dim)
217
+ x_recon = out_flat.view(batch_size, self.seq_len, self.input_dim)
218
+ return x_recon
219
+
220
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
221
+ ae_model = SequenceAutoencoder(
222
+ seq_len=SEQ_LEN,
223
+ input_dim=INPUT_SIZE,
224
+ latent_dim=LATENT_DIM
225
+ ).to(device)
226
+
227
+ ae_criterion = nn.MSELoss()
228
+ ae_optimizer = torch.optim.Adam(ae_model.parameters(), lr=AE_LR)
229
+
230
+ # ============================================
231
+ # 8. HUẤN LUYỆN AUTOENCODER
232
+ # ============================================
233
+
234
+ best_val_loss = float("inf")
235
+ best_ae_path = "best_sequence_autoencoder.pth"
236
+
237
+ for epoch in range(1, AE_EPOCHS + 1):
238
+ # --- Train ---
239
+ ae_model.train()
240
+ train_loss_sum = 0.0
241
+ for x_batch, _ in train_ae_loader:
242
+ # x_batch: shape (batch, seq_len, input_dim)
243
+ x_batch = x_batch.to(device)
244
+
245
+ ae_optimizer.zero_grad()
246
+ x_recon = ae_model(x_batch)
247
+ loss = ae_criterion(x_recon, x_batch)
248
+ loss.backward()
249
+ ae_optimizer.step()
250
+
251
+ train_loss_sum += loss.item() * x_batch.size(0)
252
+ train_loss = train_loss_sum / len(train_ae_loader.dataset)
253
+
254
+ # --- Validate ---
255
+ ae_model.eval()
256
+ val_loss_sum = 0.0
257
+ with torch.no_grad():
258
+ for x_batch, _ in val_ae_loader:
259
+ x_batch = x_batch.to(device)
260
+ x_recon = ae_model(x_batch)
261
+ loss = ae_criterion(x_recon, x_batch)
262
+ val_loss_sum += loss.item() * x_batch.size(0)
263
+ val_loss = val_loss_sum / len(val_ae_loader.dataset)
264
+
265
+ print(f"Epoch {epoch:02d} | AE Train Loss: {train_loss:.6f} | AE Val Loss: {val_loss:.6f}")
266
+
267
+ if val_loss < best_val_loss:
268
+ best_val_loss = val_loss
269
+ torch.save(ae_model.state_dict(), best_ae_path)
270
+
271
+ # Load lại AE tốt nhất
272
+ ae_model.load_state_dict(torch.load(best_ae_path, map_location=device))
273
+
274
+ # ============================================
275
+ # 9. TÍNH RECONSTRUCTION ERRORS TRÊN TEST
276
+ # ============================================
277
+
278
+ # Đầu tiên, lấy reconstruction errors cho tập validation normal (dùng để tính threshold)
279
+ val_norm_errors = []
280
+ ae_model.eval()
281
+ with torch.no_grad():
282
+ for x_batch, _ in val_ae_loader:
283
+ x_batch = x_batch.to(device)
284
+ x_recon = ae_model(x_batch)
285
+ # Tính MSE mỗi sequence riêng:
286
+ batch_errors = torch.mean((x_recon - x_batch) ** 2, dim=(1,2)) # (batch,)
287
+ val_norm_errors.append(batch_errors.cpu().numpy())
288
+ val_norm_errors = np.concatenate(val_norm_errors, axis=0)
289
+
290
+ # Threshold = mean(val_norm_errors) + k * std(val_norm_errors)
291
+ mu_val = np.mean(val_norm_errors)
292
+ sigma_val = np.std(val_norm_errors)
293
+ threshold = mu_val + THRESHOLD_STD_FACTOR * sigma_val
294
+
295
+ print(f"\nThreshold (mean + {THRESHOLD_STD_FACTOR}*std) từ tập validation normal: {threshold:.6f}")
296
+
297
+ # Bây giờ, tính reconstruction errors trên toàn bộ test set (bao gồm normal & anomaly)
298
+ test_errors = []
299
+ ae_model.eval()
300
+ with torch.no_grad():
301
+ for x_batch, _ in test_loader:
302
+ x_batch = x_batch.to(device)
303
+ x_recon = ae_model(x_batch)
304
+ batch_errors = torch.mean((x_recon - x_batch) ** 2, dim=(1,2))
305
+ test_errors.append(batch_errors.cpu().numpy())
306
+ test_errors = np.concatenate(test_errors, axis=0) # shape = (num_test_items,)
307
+
308
+ # Đánh dấu anomaly nếu error > threshold
309
+ anomalies = test_errors > threshold
310
+ num_anomalies = np.sum(anomalies)
311
+ print(f"Phát hiện {num_anomalies} samples bất thường trong tập test (trên tổng {len(test_errors)})")
312
+ print("Chỉ số sample bất thường (relative to test set):", np.where(anomalies)[0])
313
+
314
+ # ============================================
315
+ # 10. ĐÁNH GIÁ KẾT QUẢ ANOMALY DETECTION
316
+ # ============================================
317
+
318
+ # Tạo y_true_binary dựa trên y_seq cho test_idx
319
+ y_true = []
320
+ for idx in test_idx:
321
+ idx_label = idx + SEQUENCE_LENGTH
322
+ y_true.append(1 if labels_all[idx_label] == 2 else 0)
323
+ y_true = np.array(y_true, dtype=int)
324
+ y_pred = anomalies.astype(int)
325
+
326
+ # Tính confusion matrix và các metrics
327
+ cm = confusion_matrix(y_true, y_pred)
328
+ tn, fp, fn, tp = cm.ravel()
329
+
330
+ precision = precision_score(y_true, y_pred, zero_division=0)
331
+ recall = recall_score(y_true, y_pred, zero_division=0)
332
+ f1 = f1_score(y_true, y_pred, zero_division=0)
333
+ accuracy = accuracy_score(y_true, y_pred)
334
+
335
+ print("\n=== Confusion Matrix ===")
336
+ print(cm)
337
+ print(f"TN: {tn}, FP: {fp}")
338
+ print(f"FN: {fn}, TP: {tp}\n")
339
+
340
+ print("=== Metrics for Anomaly Detection ===")
341
+ print(f"Accuracy : {accuracy:.4f}")
342
+ print(f"Precision: {precision:.4f}")
343
+ print(f"Recall : {recall:.4f}")
344
+ print(f"F1-score : {f1:.4f}\n")
345
+
346
+ print("=== Classification Report ===")
347
+ print(
348
+ classification_report(
349
+ y_true,
350
+ y_pred,
351
+ target_names=["Normal (0)", "Anomaly (1)"],
352
+ zero_division=0
353
+ )
354
+ )
main_LSTM-Forecaster.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.preprocessing import MinMaxScaler
5
+ from sklearn.model_selection import StratifiedShuffleSplit
6
+ from sklearn.metrics import (
7
+ confusion_matrix,
8
+ precision_score,
9
+ recall_score,
10
+ f1_score,
11
+ accuracy_score,
12
+ classification_report
13
+ )
14
+ import torch
15
+ import torch.nn as nn
16
+ from torch.utils.data import Dataset, DataLoader, Subset
17
+
18
+ # ============================================
19
+ # 1. CÀI ĐẶT CÁC THAM SỐ CHUNG
20
+ # ============================================
21
+
22
+ # Đường dẫn tới file Excel
23
+ DATA_PATH = "dataset.xlsx"
24
+
25
+ # Tên các cột đặc trưng (không bao gồm cột label "Water Quality")
26
+ FEATURE_COLUMNS = [
27
+ "Temp",
28
+ "Turbidity (cm)",
29
+ "DO(mg/L)",
30
+ "BOD (mg/L)",
31
+ "CO2",
32
+ "pH",
33
+ "Alkalinity (mg L-1 )",
34
+ "Hardness (mg L-1 )",
35
+ "Calcium (mg L-1 )",
36
+ "Ammonia (mg L-1 )",
37
+ "Nitrite (mg L-1 )",
38
+ "Phosphorus (mg L-1 )",
39
+ "H2S (mg L-1 )",
40
+ "Plankton (No. L-1)"
41
+ ]
42
+ LABEL_COL = "Water Quality"
43
+
44
+ # Độ dài sequence (sliding window)
45
+ SEQUENCE_LENGTH = 10
46
+
47
+ # Tỉ lệ chia train / validation / test
48
+ TRAIN_RATIO = 0.8
49
+ VAL_RATIO = 0.1
50
+ TEST_RATIO = 0.1
51
+
52
+ # Hyper-parameters cho mô hình LSTM
53
+ INPUT_SIZE = len(FEATURE_COLUMNS) # 14
54
+ HIDDEN_SIZE = 64
55
+ NUM_LAYERS = 2
56
+ BATCH_SIZE = 64
57
+ NUM_EPOCHS = 50
58
+ LEARNING_RATE = 1e-3
59
+ RANDOM_STATE = 42
60
+
61
+ # ============================================
62
+ # 2. ĐỌC VÀ TIỀN XỬ LÝ DỮ LIỆU
63
+ # ============================================
64
+
65
+ # 2.1 Đọc toàn bộ DataFrame, loại bỏ các hàng hoàn toàn rỗng
66
+ df = pd.read_excel(DATA_PATH)
67
+ df = df.dropna(how="all")
68
+
69
+ # 2.2 Xử lý cột label
70
+ df[LABEL_COL] = df[LABEL_COL].astype(int)
71
+ labels_all = df[LABEL_COL].values # mảng nhãn gốc, shape = (num_total,)
72
+
73
+ # 2.3 Chuyển đổi các cột đặc trưng:
74
+ # - Thay dấu phẩy thành dấu chấm (nếu có)
75
+ # - Chuyển sang float
76
+ for col in FEATURE_COLUMNS:
77
+ if df[col].dtype == object or df[col].dtype == str:
78
+ df[col] = df[col].apply(lambda x: str(x).replace(",", "."))
79
+ df[col] = df[col].astype(float)
80
+
81
+ # 2.4 Lấy mảng dữ liệu số (chỉ gồm các cột FEATURE_COLUMNS)
82
+ data_raw = df[FEATURE_COLUMNS].values # shape = (num_total, 14)
83
+
84
+ # 2.5 Chuẩn hóa Min-Max
85
+ scaler = MinMaxScaler()
86
+ data_scaled = scaler.fit_transform(data_raw) # shape = (num_total, 14)
87
+
88
+ # ============================================
89
+ # 3. TẠO CLASS Dataset CHO TIME-SERIES
90
+ # ============================================
91
+
92
+ class TimeSeriesDataset(Dataset):
93
+ """
94
+ Dataset trả về:
95
+ - x: một cửa sổ (sequence) có shape (sequence_length, num_features)
96
+ - y: vector giá trị next step có shape (num_features,)
97
+ """
98
+ def __init__(self, data, seq_len):
99
+ """
100
+ data: numpy array shape (num_samples, num_features), đã chuẩn hóa
101
+ seq_len: độ dài của x sequence
102
+ """
103
+ self.data = data
104
+ self.seq_len = seq_len
105
+ # Số lượng item thực tế = num_samples - seq_len
106
+ self.num_items = data.shape[0] - seq_len
107
+
108
+ def __len__(self):
109
+ return self.num_items
110
+
111
+ def __getitem__(self, idx):
112
+ # x: data[idx : idx + seq_len], y: data[idx + seq_len]
113
+ x = self.data[idx : idx + self.seq_len] # shape = (seq_len, num_features)
114
+ y = self.data[idx + self.seq_len] # shape = (num_features,)
115
+ x = torch.tensor(x, dtype=torch.float32)
116
+ y = torch.tensor(y, dtype=torch.float32)
117
+ return x, y
118
+
119
+ # ============================================
120
+ # 4. TẠO MẢNG NHÃN CHO MỖI SEQUENCE (DỰA TRÊN Water Quality)
121
+ # ============================================
122
+
123
+ num_total = data_scaled.shape[0]
124
+ num_items = num_total - SEQUENCE_LENGTH # số lượng sequence-to-one
125
+
126
+ # Tạo mảng y_seq: nhãn Water Quality tại vị trí i + SEQUENCE_LENGTH
127
+ y_seq = np.zeros(num_items, dtype=int)
128
+ for i in range(num_items):
129
+ idx_label = i + SEQUENCE_LENGTH
130
+ y_seq[i] = labels_all[idx_label] # 0, 1, hoặc 2
131
+
132
+ # ============================================
133
+ # 5. CHIA DỮ LIỆU CÓ PHÂN LỚP (STRATIFIED SPLIT) THEO y_seq
134
+ # ============================================
135
+
136
+ # 5.1 Chia test (10%) trước
137
+ sss1 = StratifiedShuffleSplit(n_splits=1, test_size=TEST_RATIO, random_state=RANDOM_STATE)
138
+ for train_val_idx, test_idx in sss1.split(np.zeros(num_items), y_seq):
139
+ pass
140
+
141
+ # 5.2 Từ phần train_val (90%), chia tiếp thành train (80%) và val (10%)
142
+ train_val_labels = y_seq[train_val_idx]
143
+ val_size_relative = VAL_RATIO / (TRAIN_RATIO + VAL_RATIO)
144
+ sss2 = StratifiedShuffleSplit(n_splits=1, test_size=val_size_relative, random_state=RANDOM_STATE)
145
+ for train_idx_rel, val_idx_rel in sss2.split(np.zeros(len(train_val_idx)), train_val_labels):
146
+ pass
147
+
148
+ # Chuyển index tương đối sang index tuyệt đối trong num_items
149
+ train_idx = train_val_idx[train_idx_rel]
150
+ val_idx = train_val_idx[val_idx_rel]
151
+
152
+ # In ra số lượng mỗi nhãn trong từng tập
153
+ def count_labels(indices, y):
154
+ unique, counts = np.unique(y[indices], return_counts=True)
155
+ return dict(zip(unique.tolist(), counts.tolist()))
156
+
157
+ print("Phân bố nhãn trong tập train:", count_labels(train_idx, y_seq))
158
+ print("Phân bố nhãn trong tập val :", count_labels(val_idx, y_seq))
159
+ print("Phân bố nhãn trong tập test :", count_labels(test_idx, y_seq))
160
+
161
+ # ============================================
162
+ # 6. TẠO DataLoader CHO Train/Val/Test
163
+ # ============================================
164
+
165
+ dataset_all = TimeSeriesDataset(data_scaled, SEQUENCE_LENGTH)
166
+
167
+ train_dataset = Subset(dataset_all, train_idx)
168
+ val_dataset = Subset(dataset_all, val_idx)
169
+ test_dataset = Subset(dataset_all, test_idx)
170
+
171
+ train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
172
+ val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
173
+ test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=False)
174
+
175
+ # ============================================
176
+ # 7. XÂY DỰNG MÔ HÌNH LSTM FORECASTER
177
+ # ============================================
178
+
179
+ class LSTMForecaster(nn.Module):
180
+ def __init__(self, input_size, hidden_size, num_layers):
181
+ super(LSTMForecaster, self).__init__()
182
+ self.lstm = nn.LSTM(
183
+ input_size=input_size,
184
+ hidden_size=hidden_size,
185
+ num_layers=num_layers,
186
+ batch_first=True
187
+ )
188
+ self.linear = nn.Linear(hidden_size, input_size)
189
+
190
+ def forward(self, x):
191
+ """
192
+ x: shape (batch_size, seq_len, input_size)
193
+ Trả về y_pred: shape (batch_size, input_size)
194
+ """
195
+ out, (hn, cn) = self.lstm(x)
196
+ last_hidden = hn[-1]
197
+ y_pred = self.linear(last_hidden)
198
+ return y_pred
199
+
200
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
201
+ model = LSTMForecaster(
202
+ input_size=INPUT_SIZE,
203
+ hidden_size=HIDDEN_SIZE,
204
+ num_layers=NUM_LAYERS
205
+ ).to(device)
206
+
207
+ criterion = nn.MSELoss()
208
+ optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
209
+
210
+ # ============================================
211
+ # 8. HÀM HUẤN LUYỆN VÀ EVALUATION
212
+ # ============================================
213
+
214
+ def train_one_epoch(model, loader, criterion, optimizer, device):
215
+ model.train()
216
+ epoch_loss = 0.0
217
+ for x_batch, y_batch in loader:
218
+ x_batch = x_batch.to(device)
219
+ y_batch = y_batch.to(device)
220
+
221
+ optimizer.zero_grad()
222
+ y_pred = model(x_batch)
223
+ loss = criterion(y_pred, y_batch)
224
+ loss.backward()
225
+ optimizer.step()
226
+
227
+ epoch_loss += loss.item() * x_batch.size(0)
228
+ return epoch_loss / len(loader.dataset)
229
+
230
+ def evaluate(model, loader, criterion, device):
231
+ model.eval()
232
+ epoch_loss = 0.0
233
+ with torch.no_grad():
234
+ for x_batch, y_batch in loader:
235
+ x_batch = x_batch.to(device)
236
+ y_batch = y_batch.to(device)
237
+ y_pred = model(x_batch)
238
+ loss = criterion(y_pred, y_batch)
239
+ epoch_loss += loss.item() * x_batch.size(0)
240
+ return epoch_loss / len(loader.dataset)
241
+
242
+ # ============================================
243
+ # 9. VÒNG LẶP HUẤN LUYỆN CHÍNH
244
+ # ============================================
245
+
246
+ best_val_loss = float("inf")
247
+ best_model_path = "best_lstm_forecaster.pth"
248
+
249
+ for epoch in range(1, NUM_EPOCHS + 1):
250
+ train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
251
+ val_loss = evaluate(model, val_loader, criterion, device)
252
+
253
+ print(f"Epoch {epoch:02d} | Train Loss: {train_loss:.6f} | Val Loss: {val_loss:.6f}")
254
+
255
+ if val_loss < best_val_loss:
256
+ best_val_loss = val_loss
257
+ torch.save(model.state_dict(), best_model_path)
258
+
259
+ # Tải lại mô hình tốt nhất
260
+ model.load_state_dict(torch.load(best_model_path, map_location=device))
261
+
262
+ # ============================================
263
+ # 10. ĐÁNH GIÁ TRÊN TẬP TEST & PHÁT HIỆN BẤT THƯỜNG
264
+ # ============================================
265
+
266
+ # 10.1 Tính test loss
267
+ test_loss = evaluate(model, test_loader, criterion, device)
268
+ print(f"\nTest Loss: {test_loss:.6f}")
269
+
270
+ # 10.2 Tính lỗi MSE cho từng sample trong test
271
+ all_errors = []
272
+ model.eval()
273
+ with torch.no_grad():
274
+ for x_batch, y_batch in test_loader:
275
+ x_batch = x_batch.to(device)
276
+ y_batch = y_batch.to(device)
277
+ y_pred = model(x_batch)
278
+ batch_errors = torch.mean((y_pred - y_batch) ** 2, dim=1) # shape = (batch,)
279
+ all_errors.append(batch_errors.cpu().numpy())
280
+ all_errors = np.concatenate(all_errors, axis=0) # shape = (num_test_items,)
281
+
282
+ # 10.3 Xác định ngưỡng threshold
283
+ mu = np.mean(all_errors)
284
+ sigma = np.std(all_errors)
285
+
286
+ # Chọn ngưỡng mean + 2*std (thay vì 3*std) để tăng recall
287
+ threshold = mu + 2 * sigma
288
+
289
+ # Nếu muốn, có thể thử percentile, ví dụ 95th percentile:
290
+ # threshold = np.percentile(all_errors, 95)
291
+
292
+ anomalies = all_errors > threshold # bool array
293
+ num_anomalies = np.sum(anomalies)
294
+ print(f"Threshold (mean + 2*std): {threshold:.6f}")
295
+ print(f"Phát hiện {num_anomalies} samples bất thường trong tập test (trên tổng {len(all_errors)})")
296
+ print("Chỉ số sample bất thường (relative to test set):", np.where(anomalies)[0])
297
+
298
+ # ============================================
299
+ # 11. TÍNH CÁC CHỈ SỐ ĐÁNH GIÁ ANOMALY DETECTION
300
+ # ============================================
301
+
302
+ # 11.1 Xây mảng y_true_binary tương ứng với mỗi sequence trong test:
303
+ # water quality = 2 → anomaly (1); = 0 hoặc 1 → normal (0)
304
+ y_true = []
305
+ for idx in test_idx:
306
+ idx_label = idx + SEQUENCE_LENGTH
307
+ y_true.append(1 if labels_all[idx_label] == 2 else 0)
308
+ y_true = np.array(y_true, dtype=int)
309
+
310
+ # 11.2 Chuyển anomalies (bool) sang y_pred (0/1)
311
+ y_pred = anomalies.astype(int)
312
+
313
+ # 11.3 Tính confusion matrix và metrics
314
+ cm = confusion_matrix(y_true, y_pred)
315
+ tn, fp, fn, tp = cm.ravel()
316
+
317
+ precision = precision_score(y_true, y_pred, zero_division=0)
318
+ recall = recall_score(y_true, y_pred, zero_division=0)
319
+ f1 = f1_score(y_true, y_pred, zero_division=0)
320
+ accuracy = accuracy_score(y_true, y_pred)
321
+
322
+ print("\n=== Confusion Matrix ===")
323
+ print(cm)
324
+ print(f"TN: {tn}, FP: {fp}")
325
+ print(f"FN: {fn}, TP: {tp}\n")
326
+
327
+ print("=== Metrics for Anomaly Detection ===")
328
+ print(f"Accuracy : {accuracy:.4f}")
329
+ print(f"Precision: {precision:.4f}")
330
+ print(f"Recall : {recall:.4f}")
331
+ print(f"F1-score : {f1:.4f}\n")
332
+
333
+ print("=== Classification Report ===")
334
+ print(
335
+ classification_report(
336
+ y_true,
337
+ y_pred,
338
+ target_names=["Normal (0)", "Anomaly (1)"],
339
+ zero_division=0
340
+ )
341
+ )