Upload folder using huggingface_hub
Browse files- CNN-Autoencoder.py +320 -0
- Report/CNN-Autoencoder.txt +32 -0
- Report/Fully‐Connected-Sequence-Autoencoder.txt +71 -0
- Report/LSTM-Forecaster.txt +47 -0
- Report/transformer_ae_results.txt +29 -0
- Report/vae_results.txt +29 -0
- Transformer‐AE.py +292 -0
- VAE.py +272 -0
- best_cnn_ae.pth +3 -0
- best_lstm_ae.pth +3 -0
- best_lstm_forecaster.pth +3 -0
- best_sequence_autoencoder.pth +3 -0
- best_transformer_ae.pth +3 -0
- best_vae.pth +3 -0
- main_Fully‐Connected-Sequence-Autoencoder.py +354 -0
- main_LSTM-Forecaster.py +341 -0
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 |
+
)
|