Bennie12 commited on
Commit
0e017ad
·
verified ·
1 Parent(s): 6b33853

Update AI_Model_architecture.py

Browse files
Files changed (1) hide show
  1. AI_Model_architecture.py +154 -493
AI_Model_architecture.py CHANGED
@@ -1,494 +1,155 @@
1
- """
2
- 流程圖
3
- 讀取資料 → 分割資料 → 編碼 → 建立 Dataset / DataLoader
4
-
5
- 建立模型(BERT+LSTM+CNN)
6
-
7
- BERT 輸出 [batch, seq_len, 768]
8
-
9
- BiLSTM [batch, seq_len, hidden_dim*2]
10
-
11
- CNN 模組 (Conv1D + Dropout + GlobalMaxPooling1D)
12
-
13
- Linear 分類器(輸出詐騙機率)
14
-
15
- 訓練模型(Epochs)
16
-
17
- 評估模型(Accuracy / F1 / Precision / Recall)
18
-
19
- 儲存模型(.pth)
20
-
21
- """
22
- #引入重要套件Import Library
23
-
24
- import os
25
- import torch # PyTorch 主模組
26
- import torch.nn as nn # 神經網路相關的層(例如 LSTM、Linear)
27
- import pandas as pd
28
- import re
29
- import ast
30
- import matplotlib.pyplot as plt
31
- import seaborn as sns
32
-
33
- from sklearn.metrics import (
34
- classification_report, confusion_matrix, accuracy_score,
35
- precision_score, recall_score, f1_score, roc_auc_score,
36
- precision_recall_curve, auc, matthews_corrcoef
37
- )
38
- from dotenv import load_dotenv
39
- from sklearn.model_selection import train_test_split
40
- from torch.utils.data import DataLoader, Dataset # 提供 Dataset、DataLoader 類別
41
- from transformers import BertTokenizer # BertTokenizer把文字句子轉換成 BERT 格式的 token ID,例如 [CLS] 今天 天氣 不錯 [SEP] → [101, 1234, 5678, ...]
42
- from sklearn.model_selection import train_test_split
43
-
44
- from transformers import BertModel
45
- """
46
- # ------------------- 載入 .env 環境變數 -------------------
47
-
48
- path = r"E:\Project_PredictScamInfo"
49
-
50
- # ------------------- 使用相對路徑找 CSV -------------------
51
-
52
- #如有需要訓練複數筆資料可以使用這個方法csv_files = [os.path.join(base_dir, "檔案名稱1.csv"),os.path.join(base_dir, "檔案名稱2.csv")]
53
- #程式碼一至131行
54
-
55
- # GPU 記憶體限制(可選)
56
- # os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:16"
57
-
58
- #資料前處理
59
-
60
- class BertPreprocessor:
61
- def __init__(self, tokenizer_name="ckiplab/bert-base-chinese", max_len=128):
62
- self.tokenizer = BertTokenizer.from_pretrained(tokenizer_name)
63
- self.max_len = max_len
64
-
65
- def load_and_clean(self, filepath):
66
- df = pd.read_csv(filepath)
67
- df = df.dropna().drop_duplicates(subset=["message"]).reset_index(drop=True)
68
-
69
- # 清理 message 欄位
70
- df["message"] = df["message"].astype(str)
71
- df["message"] = df["message"].apply(lambda text: re.sub(r"\s+", "", text))
72
- df["message"] = df["message"].apply(lambda text: re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", text))
73
-
74
- # 清理 keywords 欄位(如果有)
75
- if "keywords" in df.columns:
76
- df["keywords"] = df["keywords"].fillna("")
77
- df["keywords"] = df["keywords"].apply(
78
- lambda x: ast.literal_eval(x) if isinstance(x, str) and x.startswith("[") else []
79
- )
80
- df["keywords"] = df["keywords"].apply(
81
- lambda lst: [re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", str(k)) for k in lst]
82
- )
83
- df["keywords"] = df["keywords"].apply(lambda lst: "。".join(lst))
84
- else:
85
- df["keywords"] = ""
86
-
87
- # 合併為 BERT 輸入內容
88
- df["combined"] = df["message"] + "。" + df["keywords"]
89
- return df[["combined", "label"]]
90
-
91
- def encode(self, texts):
92
- return self.tokenizer(
93
- list(texts),
94
- return_tensors="pt",
95
- truncation=True,
96
- padding="max_length",
97
- max_length=self.max_len
98
- )
99
- #自動做資料前處理
100
- def build_bert_inputs(files):
101
- processor = BertPreprocessor()
102
- dfs = [processor.load_and_clean(f) for f in files]
103
- all_df = pd.concat(dfs, ignore_index=True)
104
- print("📌 原始資料筆數:", sum(len(pd.read_csv(f)) for f in files))
105
- print("📌 清理後資料筆數:", len(all_df))
106
- print(f"✅ 已讀入 {len(all_df)} 筆資料")
107
- print(all_df["label"].value_counts())
108
- print("📌 合併後輸入示例:")
109
- print(all_df["combined"].head())
110
-
111
- train_df, val_df = train_test_split(
112
- all_df,
113
- stratify=all_df["label"],
114
- test_size=0.2,
115
- random_state=25,
116
- shuffle=True
117
- )
118
-
119
- train_inputs = processor.encode(train_df["combined"])
120
- val_inputs = processor.encode(val_df["combined"])
121
-
122
- return train_inputs, train_df["label"], val_inputs, val_df["label"], processor
123
-
124
-
125
- #定義 PyTorch Dataset 類別。ScamDataset 繼承自 torch.utils.data.Dataset
126
- #將 BERT 輸出的 token 與對應標籤封裝成 PyTorch 能使用的格式
127
- class ScamDataset(Dataset):
128
- def __init__(self, inputs, labels):
129
- self.input_ids = inputs["input_ids"] # input_ids:句子的 token ID;attention_mask:注意力遮罩(0 = padding)
130
- self.attention_mask = inputs["attention_mask"] # token_type_ids:句子的 segment 區分
131
- self.token_type_ids = inputs["token_type_ids"] # torch.tensor(x, dtype=...)將資料(x)轉為Tensor的標準做法。
132
- self.labels = torch.tensor(labels.values, dtype=torch.float32) # x可以是 list、NumPy array、pandas series...
133
- # dtypefloat32:浮點數(常用於 回歸 或 BCELoss 二分類);long:整數(常用於 多分類 搭配 CrossEntropyLoss)。labels.values → 轉為 NumPy array
134
-
135
- def __len__(self): # 告訴 PyTorch 這個 Dataset 有幾筆資料
136
- return len(self.labels) # 給 len(dataset) 或 for i in range(len(dataset)) 用的
137
-
138
- def __getitem__(self, idx): #每次調用 __getitem__() 回傳一筆 {input_ids, attention_mask, token_type_ids, labels}
139
- return { #DataLoader 每次會呼叫這個方法多次來抓一個 batch 的資料
140
- "input_ids":self.input_ids[idx],
141
- "attention_mask":self.attention_mask[idx],
142
- "token_type_ids":self.token_type_ids[idx],
143
- "labels":self.labels[idx]
144
- }
145
-
146
- # 這樣可以同時處理 scam 和 normal 資料,不用重複寫清理與 token 處理
147
- if __name__ == "__main__":
148
- csv_files = [os.path.join(path, r"Filled_Keyword_MessageDeduplicated.csv")]
149
- train_inputs, train_labels, val_inputs, val_labels, processor = build_bert_inputs(csv_files)
150
-
151
- train_dataset = ScamDataset(train_inputs, train_labels)
152
- val_dataset = ScamDataset(val_inputs, val_labels)
153
-
154
- # batch_size每次送進模型的是 8 筆資料(而不是一筆一筆)
155
- # 每次從 Dataset 中抓一批(batch)資料出來
156
- train_loader = DataLoader(train_dataset, batch_size=128)
157
- val_loader = DataLoader(val_dataset, batch_size=128)
158
- """
159
- """
160
- class BertLSTM_CNN_Classifier(nn.Module)表示:你定義了一個子類別,
161
- 繼承自 PyTorch 的基礎模型類別 nn.Module。
162
-
163
- 若你在 __init__() 裡沒有呼叫 super().__init__(),
164
- 那麼父類別 nn.Module 的初始化邏輯(包含重要功能)就不會被執行,
165
- 導致整個模型運作異常或錯誤。
166
- """
167
-
168
- # nn.Module是PyTorch所有神經網路模型的基礎類別,nn.Module 是 PyTorch 所有神經網路模型的基礎類別
169
- class BertLSTM_CNN_Classifier(nn.Module):
170
-
171
- def __init__(self, hidden_dim=128, num_layers=1, dropout=0.3):
172
-
173
- # super()是Python提供的一個方法,用來呼叫「父類別的版本」的方法。
174
- # 呼叫:super().__init__()讓父類別(nn.Module)裡面那些功能、屬性都被正確初始化。
175
- # 沒super().__init__(),這些都不會正確運作,模型會壞掉。
176
- # super() 就是 Python 提供給「子類別呼叫父類別方法」的方式
177
- super().__init__()
178
-
179
- # 載入中文預訓練的 BERT 模型,輸入為句子token IDs,輸出為每個 token 的向量,大小為 [batch, seq_len, 768]。
180
- self.bert = BertModel.from_pretrained("ckiplab/bert-base-chinese") # 這是引入hugging face中的tranceformat
181
-
182
- # 接收BERT的輸出(768 維向量),進行雙向LSTM(BiLSTM)建模,輸出為 [batch, seq_len, hidden_dim*2],例如 [batch, seq_len, 256]
183
- """
184
- LSTM 接收每個token的768維向量(來自 BERT)作為輸入,
185
- 透過每個方向的LSTM壓縮成128維的語意向量。
186
- 由於是雙向LSTM,會同時從左到右(前向)和右到左(後向)各做一次,
187
- 最後將兩個方向的輸出合併為256維向量(128×2)。
188
- 每次處理一個 batch(例如 8 句話),一次走完整個時間序列。
189
- """
190
- self.LSTM = nn.LSTM(input_size=768,
191
- hidden_size=hidden_dim,
192
- num_layers=num_layers,
193
- batch_first=True,
194
- bidirectional=True)
195
-
196
- # CNN 模組:接在 LSTM 後的輸出上。將LSTM的輸出轉成卷積層格式,適用於Conv1D,CNN可學習位置不變的局部特徵。
197
- self.conv1 = nn.Conv1d(in_channels=hidden_dim*2,
198
- out_channels=128,
199
- kernel_size=3, # 這裡kernel_size=3 為 3-gram 特徵
200
- padding=1)
201
-
202
- self.dropout = nn.Dropout(dropout) # 隨機將部分神經元設為 0,用來防止 overfitting。
203
-
204
- self.global_maxpool = nn.AdaptiveAvgPool1d(1) #將一整句話的特徵濃縮成一個固定大小的句子表示向量
205
-
206
- # 將CNN輸出的128維特徵向量輸出為一個「機率值」(詐騙或非詐騙)。
207
- self.classifier = nn.Linear(128,1)
208
-
209
- def forward(self, input_ids, attention_mask, token_type_ids):
210
- #BERT 編碼
211
- outputs = self.bert(input_ids=input_ids,
212
- attention_mask=attention_mask,
213
- token_type_ids=token_type_ids)
214
- #.last_hidden_state是BertModel.from_pretrained(...)內部的key,會輸出 [batch, seq_len, 768]
215
- hidden_states = outputs.last_hidden_state
216
-
217
- # 送入 BiLSTM
218
- # transpose(1, 2) 的用途是:讓 LSTM 輸出的資料形狀符合 CNN 所要求的格式
219
- # 假設你原本 LSTM 輸出是: [batch_size, seq_len, hidden_dim*2] = [8, 128, 256]
220
- # 但CNN(Conv1d)的輸入格式需要是:[batch_size, in_channels, seq_len] = [8, 256, 128]
221
- # 因此你需要做:.transpose(1, 2)把 seq_len 和 hidden_dim*2 調換
222
- LSTM_out, _ = self.LSTM(hidden_states) # [batch, seq_len, hidden_dim*2]
223
- LSTM_out = LSTM_out.transpose(1, 2) # [batch, hidden_dim*2, seq_len]
224
-
225
- # 卷積 + Dropout
226
- x = self.conv1(LSTM_out) # [batch, 128, seq_len]
227
- x = self.dropout(x)
228
-
229
- #全局池化
230
- # .squeeze(dim) 的作用是:把某個「維度大小為 1」的維度刪掉
231
- # x = self.global_maxpool(x).squeeze(2) # 輸出是 [batch, 128, 1]
232
- # 不 .squeeze(2),你會得到 shape 為 [batch, 128, 1],不方便後面接 Linear。
233
- # .squeeze(2)=拿掉第 2 維(數值是 1) → 讓形狀變成 [batch, 128]
234
- x = self.global_maxpool(x).squeeze(2) # [batch, 128]
235
-
236
- #分類 & Sigmoid 機率輸出
237
- logits = self.classifier(x)
238
-
239
- #.sigmoid() → 把 logits 轉成 0~1 的機率.squeeze() → 變成一維 [batch] 長度的機率 list
240
- """例如:
241
- logits = [[0.92], [0.05], [0.88], [0.41], ..., [0.17]]
242
- → sigmoid → [[0.715], [0.512], ...]
243
- → squeeze → [0.715, 0.512, ...]
244
- """
245
- return logits.squeeze() # 最後輸出是一個值介於 0 ~ 1 之間,代表「為詐騙訊息的機率」。
246
- """
247
- # 設定 GPU 裝置
248
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
249
-
250
- # 初始化模型
251
- model = BertLSTM_CNN_Classifier().to(device)
252
- # 定義 optimizer 和損失函數
253
- optimizer = torch.optim.Adam(model.parameters(),lr=2e-5)
254
- pos_weight = torch.tensor([2.13], dtype=torch.float32).to(device)
255
- criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
256
-
257
- # 本機訓練迴圈,要訓練再取消註解,否則在線上版本一律處於註解狀態
258
- # 訓練期間用的簡化版驗證函式 (只回傳 loss / acc)
259
- def evaluate_epoch(model, dataloader, criterion, device):
260
- model.eval()
261
- total_loss = 0
262
- all_labels, all_preds = [], []
263
-
264
- with torch.no_grad():
265
- for batch in dataloader:
266
- input_ids = batch["input_ids"].to(device)
267
- attention_mask = batch["attention_mask"].to(device)
268
- token_type_ids = batch["token_type_ids"].to(device)
269
- labels = batch["labels"].to(device)
270
-
271
- outputs = model(input_ids, attention_mask, token_type_ids)
272
- loss = criterion(outputs, labels)
273
- total_loss += loss.item()
274
-
275
- preds = (torch.sigmoid(outputs) > 0.5).long()
276
- all_labels.extend(labels.cpu().numpy())
277
- all_preds.extend(preds.cpu().numpy())
278
-
279
- acc = accuracy_score(all_labels, all_preds)
280
- avg_loss = total_loss / len(dataloader)
281
- return avg_loss, acc
282
-
283
- # 修改訓練主程式
284
-
285
- def train_model(model, train_loader, val_loader, optimizer, criterion, device, num_epochs=15, save_path="model.pth"):
286
- train_loss_list, val_loss_list = [], []
287
- train_acc_list, val_acc_list = [], []
288
-
289
- for epoch in range(num_epochs):
290
- model.train()
291
- total_train_loss = 0
292
- train_true, train_pred = [], []
293
-
294
- for batch in train_loader:
295
- optimizer.zero_grad()
296
- input_ids = batch["input_ids"].to(device)
297
- attention_mask = batch["attention_mask"].to(device)
298
- token_type_ids = batch["token_type_ids"].to(device)
299
- labels = batch["labels"].to(device)
300
-
301
- outputs = model(input_ids, attention_mask, token_type_ids)
302
- loss = criterion(outputs, labels)
303
- loss.backward()
304
- optimizer.step()
305
-
306
- total_train_loss += loss.item()
307
- preds = (torch.sigmoid(outputs) > 0.5).long()
308
- train_true.extend(labels.cpu().numpy())
309
- train_pred.extend(preds.cpu().numpy())
310
-
311
- train_acc = accuracy_score(train_true, train_pred)
312
- train_loss = total_train_loss / len(train_loader)
313
-
314
- val_loss, val_acc = evaluate_epoch(model, val_loader, criterion, device)
315
-
316
- train_loss_list.append(train_loss)
317
- val_loss_list.append(val_loss)
318
- train_acc_list.append(train_acc)
319
- val_acc_list.append(val_acc)
320
-
321
- print(f"[Epoch {epoch+1}] Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f}")
322
-
323
- torch.save(model.state_dict(), save_path)
324
- print(f"✅ 模型訓練完成並儲存為 {save_path}")
325
-
326
- # 可視化 Loss Curve
327
- plt.figure(figsize=(8, 5))
328
- plt.plot(range(1, num_epochs+1), train_loss_list, label="Train Loss")
329
- plt.plot(range(1, num_epochs+1), val_loss_list, label="Val Loss")
330
- plt.xlabel("Epoch")
331
- plt.ylabel("Loss")
332
- plt.title("Loss Curve")
333
- plt.legend()
334
- plt.show()
335
-
336
- # 可視化 Accuracy Curve
337
- plt.figure(figsize=(8, 5))
338
- plt.plot(range(1, num_epochs+1), train_acc_list, label="Train Accuracy")
339
- plt.plot(range(1, num_epochs+1), val_acc_list, label="Val Accuracy")
340
- plt.xlabel("Epoch")
341
- plt.ylabel("Accuracy")
342
- plt.title("Accuracy Curve")
343
- plt.legend()
344
- plt.show()
345
-
346
- # 訓練結束後繪製 Loss Curve
347
- plt.figure(figsize=(8, 5))
348
- plt.plot(range(1, num_epochs+1), train_loss_list, label="Train Loss")
349
- plt.plot(range(1, num_epochs+1), val_loss_list, label="Val Loss")
350
- plt.xlabel("Epoch")
351
- plt.ylabel("Loss")
352
- plt.title("Loss Curve")
353
- plt.legend()
354
- plt.show()
355
-
356
- # 繪製 Accuracy Curve
357
- plt.figure(figsize=(8, 5))
358
- plt.plot(range(1, num_epochs+1), train_acc_list, label="Train Accuracy")
359
- plt.plot(range(1, num_epochs+1), val_acc_list, label="Val Accuracy")
360
- plt.xlabel("Epoch")
361
- plt.ylabel("Accuracy")
362
- plt.title("Accuracy Curve")
363
- plt.legend()
364
- plt.show()
365
-
366
- def evaluate_model(model, val_loader, device):
367
- model.eval()
368
- y_true = []
369
- y_pred = []
370
- y_pred_prob = []
371
-
372
- with torch.no_grad():
373
- for batch in val_loader:
374
- input_ids = batch["input_ids"].to(device)
375
- attention_mask = batch["attention_mask"].to(device)
376
- token_type_ids = batch["token_type_ids"].to(device)
377
- labels = batch["labels"].to(device)
378
-
379
- outputs = model(input_ids, attention_mask, token_type_ids)
380
- probs = torch.sigmoid(outputs)
381
- preds = (probs > 0.5).long()
382
-
383
- y_true.extend(labels.cpu().numpy())
384
- y_pred.extend(preds.cpu().numpy())
385
- y_pred_prob.extend(probs.cpu().numpy())
386
-
387
- acc = accuracy_score(y_true, y_pred)
388
- prec = precision_score(y_true, y_pred)
389
- rec = recall_score(y_true, y_pred)
390
- f1 = f1_score(y_true, y_pred)
391
- spec = recall_score(y_true, y_pred, pos_label=0)
392
- mcc = matthews_corrcoef(y_true, y_pred)
393
- roc_auc = roc_auc_score(y_true, y_pred_prob)
394
- precision_curve, recall_curve, _ = precision_recall_curve(y_true, y_pred_prob)
395
- pr_auc = auc(recall_curve, precision_curve)
396
-
397
- metrics_dict = {
398
- 'Accuracy': acc,
399
- 'Precision': prec,
400
- 'Recall': rec,
401
- 'Specificity': spec,
402
- 'F1-score': f1,
403
- 'MCC': mcc,
404
- 'ROC AUC': roc_auc,
405
- 'PR AUC': pr_auc
406
- }
407
-
408
- # 視覺化:整體指標 bar chart
409
- metric_names = list(metrics_dict.keys())
410
- metric_values = list(metrics_dict.values())
411
-
412
- plt.figure(figsize=(10, 6))
413
- sns.barplot(y=metric_names, x=metric_values, palette="Blues_d")
414
- for index, value in enumerate(metric_values):
415
- plt.text(value + 0.01, index, f"{value:.4f}", va='center')
416
- plt.title("模型評估指標")
417
- plt.xlim(0, 1.05)
418
- plt.xlabel("Score")
419
- plt.ylabel("Metric")
420
- plt.grid(axis='x', linestyle='--', alpha=0.7)
421
- plt.tight_layout()
422
- plt.show()
423
-
424
- # 視覺化:Confusion Matrix
425
- cm = confusion_matrix(y_true, y_pred)
426
- plt.figure(figsize=(6, 5))
427
- sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=["Scam (0)", "Normal (1)"], yticklabels=["Scam (0)", "Normal (1)"])
428
- plt.xlabel("Predict")
429
- plt.ylabel("Actual")
430
- plt.title("Confusion Matrix")
431
- plt.tight_layout()
432
- plt.show()
433
-
434
- # PR Curve (額外 bonus)
435
- plt.plot(recall_curve, precision_curve, marker='.')
436
- plt.title('Precision-Recall Curve')
437
- plt.xlabel('Recall')
438
- plt.ylabel('Precision')
439
- plt.show()
440
-
441
- # ROC Curve (額外 bonus)
442
- from sklearn.metrics import RocCurveDisplay
443
- RocCurveDisplay.from_predictions(y_true, y_pred_prob)
444
- plt.title('ROC Curve')
445
- plt.show()
446
-
447
-
448
- # 放在主程式中呼叫
449
- if __name__ == "__main__":
450
- print("✅ 開始驗證模型效果...")
451
- evaluate_model(model, val_loader, device)
452
- """
453
- """
454
-
455
- 整個模型中每一個文字(token)始終是一個向量,隨著層數不同,這個向量代表的意義會更高階、更語意、更抽象。
456
- 在整個 BERT + LSTM + CNN 模型的流程中,「每一個文字(token)」都會被表示成一個「向量」來進行後續的計算與學習。
457
- 今天我輸入一個句子:"早安你好,吃飯沒"
458
- BERT 的輸入包含三個部分:input_ids、attention_mask、token_type_ids,
459
- 這些是 BERT 所需的格式。BERT 會將句子中每個 token 編碼為一個 768 維的語意向量,
460
-
461
- 進入 BERT → 每個 token 變成語意向量:
462
- BERT 輸出每個字為一個 768 維的語意向量
463
- 「早」 → [0.23, -0.11, ..., 0.45] 長度為 768
464
- 「安」 → [0.05, 0.33, ..., -0.12] 一樣 768
465
- ...
466
- batch size 是 8,句子長度是 8,輸出 shape 為:
467
- [batch_size=8, seq_len=8, hidden_size=768]
468
-
469
- 接下來這些向量會輸入到 LSTM,LSTM不會改變「一個token是一個向量」的概念,而是重新表示每個token的語境向量。
470
- 把每個原本 768 維的 token 壓縮成 hidden_size=128,雙向 LSTM → 拼接 → 每個 token 成為 256 維向量:
471
-
472
- input_size=768 是從 BERT 接收的向量維度
473
- hidden_size=128 表示每個方向的 LSTM 會把 token 壓縮為 128 維語意向量
474
- num_layers=1 表示只堆疊 1 層 LSTM
475
- bidirectional=True 表示是雙向
476
-
477
- LSTM,除了從左讀到右,也會從右讀到左,兩個方向的輸出會合併(拼接),變成:
478
- [batch_size=8, seq_len=8, hidden_size=256] # 因為128*2
479
-
480
- 接下來進入 CNN,CNN 仍然以「一個向量代表一個字」的形式處理:
481
-
482
- in_channels=256(因為 LSTM 是雙向輸出)
483
-
484
- out_channels=128 表示學習出 128 個濾波器,每個濾波器專門抓一種 n-gram(例如「早安你」),每個「片段」的結果輸出為 128 維特徵
485
-
486
- kernel_size=3 表示每個濾波器看 3 個連續 token(像是一個 3-gram)或,把相鄰的 3 個字(各為 256 維)一起掃描
487
-
488
- padding=1 為了保留輸出序列長度和輸入相同,避免邊界資訊被捨棄
489
-
490
- CNN 輸出的 shape 就會是:
491
-
492
- [batch_size=8, out_channels=128, seq_len=8],還是每個 token 有對應一個向量(只是這向量是 CNN 抽出的新特徵)
493
-
494
  """
 
1
+ """
2
+ 流程圖
3
+ 讀取資料 → 分割資料 → 編碼 → 建立 Dataset / DataLoader
4
+
5
+ 建立模型(BERT+LSTM+CNN)
6
+
7
+ BERT 輸出 [batch, seq_len, 768]
8
+
9
+ BiLSTM [batch, seq_len, hidden_dim*2]
10
+
11
+ CNN 模組 (Conv1D + Dropout + GlobalMaxPooling1D)
12
+
13
+ Linear 分類器(輸出詐騙機率)
14
+
15
+ 訓練模型(Epochs)
16
+
17
+ 評估模型(Accuracy / F1 / Precision / Recall)
18
+
19
+ 儲存模型(.pth)
20
+
21
+ """
22
+ #引入重要套件Import Library
23
+
24
+
25
+ import torch # PyTorch 主模組
26
+ import torch.nn as nn # 神經網路相關的層(例如 LSTM、Linear)
27
+
28
+
29
+ from transformers import BertTokenizer # BertTokenizer把文字句子轉換成 BERT 格式的 token ID,例如 [CLS] 今天 天氣 不錯 [SEP] → [101, 1234, 5678, ...]
30
+
31
+
32
+ from transformers import BertModel
33
+
34
+
35
+
36
+ # nn.Module是PyTorch所有神經網路模型的基礎類別,nn.Module 是 PyTorch 所有神經網路模型的基礎類別
37
+ class BertLSTM_CNN_Classifier(nn.Module):
38
+
39
+ def __init__(self, hidden_dim=128, num_layers=1, dropout=0.3):
40
+
41
+ # super()是Python提供的一個方法,用來呼叫「父類別的版本」的方法。
42
+ # 呼叫:super().__init__()讓父類別(nn.Module)裡面那些功能、屬性都被正確初始化。
43
+ # 沒super().__init__(),這些都不會正確運作,模型會壞掉。
44
+ # super() 就是 Python 提供給「子類別呼叫父類別方法」的方式
45
+ super().__init__()
46
+
47
+ # 載入中文預訓練的 BERT 模型,輸入為句子token IDs,輸出為每個 token 的向量,大小為 [batch, seq_len, 768]。
48
+ self.bert = BertModel.from_pretrained("ckiplab/bert-base-chinese") # 這是引入hugging face中的tranceformat
49
+
50
+ # 接收BERT的輸出(768 維向量),進行雙向LSTM(BiLSTM)建模,輸出為 [batch, seq_len, hidden_dim*2],例如 [batch, seq_len, 256]
51
+ """
52
+ LSTM 接收每個token的768維向量(來自 BERT)作為輸入,
53
+ 透過每個方向的LSTM壓縮成128維的語意向量。
54
+ 由於是雙向LSTM,會同時從左到右(前向)和右到左(後向)各做一次,
55
+ 最後將兩個方向的輸出合併為256維向量(128×2)
56
+ 每次處理一個 batch(例如 8 句話),一次走完整個時間序列。
57
+ """
58
+ self.LSTM = nn.LSTM(input_size=768,
59
+ hidden_size=hidden_dim,
60
+ num_layers=num_layers,
61
+ batch_first=True,
62
+ bidirectional=True)
63
+
64
+ # CNN 模組:接在 LSTM 後的輸出上。將LSTM的輸出轉成卷積層格式,適用於Conv1D,CNN可學習位置不變的局部特徵。
65
+ self.conv1 = nn.Conv1d(in_channels=hidden_dim*2,
66
+ out_channels=128,
67
+ kernel_size=3, # 這裡kernel_size=3 為 3-gram 特徵
68
+ padding=1)
69
+
70
+ self.dropout = nn.Dropout(dropout) # 隨機將部分神經元設為 0,用來防止 overfitting。
71
+
72
+ self.global_maxpool = nn.AdaptiveAvgPool1d(1) #將一整句話的特徵濃縮成一個固定大小的句子表示向量
73
+
74
+ # 將CNN輸出的128維特徵向量輸出為一個「機率值」(詐騙或非詐騙)。
75
+ self.classifier = nn.Linear(128,1)
76
+
77
+ def forward(self, input_ids, attention_mask, token_type_ids):
78
+ #BERT 編碼
79
+ outputs = self.bert(input_ids=input_ids,
80
+ attention_mask=attention_mask,
81
+ token_type_ids=token_type_ids)
82
+ #.last_hidden_state是BertModel.from_pretrained(...)內部的key,會輸出 [batch, seq_len, 768]
83
+ hidden_states = outputs.last_hidden_state
84
+
85
+ # 送入 BiLSTM
86
+ # transpose(1, 2) 的用途是:讓 LSTM 輸出的資料形狀符合 CNN 所要求的格式
87
+ # 假設你原本 LSTM 輸出是: [batch_size, seq_len, hidden_dim*2] = [8, 128, 256]
88
+ # 但CNN(Conv1d)的輸入格式需要是:[batch_size, in_channels, seq_len] = [8, 256, 128]
89
+ # 因此你需要做:.transpose(1, 2)把 seq_len 和 hidden_dim*2 調換
90
+ LSTM_out, _ = self.LSTM(hidden_states) # [batch, seq_len, hidden_dim*2]
91
+ LSTM_out = LSTM_out.transpose(1, 2) # [batch, hidden_dim*2, seq_len]
92
+
93
+ # 卷積 + Dropout
94
+ x = self.conv1(LSTM_out) # [batch, 128, seq_len]
95
+ x = self.dropout(x)
96
+
97
+ #全局池化
98
+ # .squeeze(dim) 的作用是:把某個「維度大小為 1」的維度刪掉
99
+ # x = self.global_maxpool(x).squeeze(2) # 輸出是 [batch, 128, 1]
100
+ # 不 .squeeze(2),你會得到 shape 為 [batch, 128, 1],不方便後面接 Linear。
101
+ # .squeeze(2)=拿掉第 2 維(數值是 1) → 讓形狀變成 [batch, 128]
102
+ x = self.global_maxpool(x).squeeze(2) # [batch, 128]
103
+
104
+ #分類 & Sigmoid 機率輸出
105
+ logits = self.classifier(x)
106
+
107
+ #.sigmoid() → 把 logits 轉成 0~1 的機率.squeeze() → 變成一維 [batch] 長度的機率 list
108
+ """例如:
109
+ logits = [[0.92], [0.05], [0.88], [0.41], ..., [0.17]]
110
+ → sigmoid → [[0.715], [0.512], ...]
111
+ → squeeze → [0.715, 0.512, ...]
112
+ """
113
+ return logits.squeeze() # 最後輸出是一個值介於 0 ~ 1 之間,代表「為詐騙訊息的機率」。
114
+ """
115
+
116
+ 整個模型中每一個文字(token)始終是一個向量,隨著層數不同,這個向量代表的意義會更高階、更語意、更抽象。
117
+ 在整個 BERT + LSTM + CNN 模型的流程中,「每一個文字(token)」都會被表示成一個「向量」來進行後續的計算與學習。
118
+ 今天我輸入一個句子:"早安你好,吃飯沒"
119
+ BERT 的輸入包含三個部分:input_ids、attention_mask、token_type_ids,
120
+ 這些是 BERT 所需的格式。BERT 會將句子中每個 token 編碼為一個 768 維的語意向量,
121
+
122
+ 進入 BERT 每個 token 變成語意向量:
123
+ BERT 輸出每個字為一個 768 維的語意向量
124
+ 「早」 → [0.23, -0.11, ..., 0.45] 長度為 768
125
+ 「安」 [0.05, 0.33, ..., -0.12] 一樣 768
126
+ ...
127
+ batch size 是 8,句子長度是 8,輸出 shape 為:
128
+ [batch_size=8, seq_len=8, hidden_size=768]
129
+
130
+ 接下來這些向量會輸入到 LSTM,LSTM不會改變「一個token是一個向量」的概念,而是重新表示每個token的語境向量。
131
+ 把每個原本 768 維的 token 壓縮成 hidden_size=128,雙向 LSTM 拼接 → 每個 token 成為 256 維向量:
132
+
133
+ input_size=768 是從 BERT 接收的向量維度
134
+ hidden_size=128 表示每個方向的 LSTM 會把 token 壓縮為 128 維語意向量
135
+ num_layers=1 表示只堆疊 1 LSTM
136
+ bidirectional=True 表示是雙向
137
+
138
+ LSTM,除了從左讀到右,也會從右讀到左,兩個方向的輸出會合併(拼接),變成:
139
+ [batch_size=8, seq_len=8, hidden_size=256] # 因為128*2
140
+
141
+ 接下來進入 CNN,CNN 仍然以「一個向量代表一個字」的形式處理:
142
+
143
+ in_channels=256(因為 LSTM 是雙向輸出)
144
+
145
+ out_channels=128 表示學習出 128 個濾波器,每個濾波器專門抓一種 n-gram(例如「早安你」),每個「片段」的結果輸出為 128 維特徵
146
+
147
+ kernel_size=3 表示每個濾波器看 3 個連續 token(像是一個 3-gram)或,把相鄰的 3 個字(各為 256 維)一起掃描
148
+
149
+ padding=1 為了保留輸出序列長度和輸入相同,避免邊界資訊被捨棄
150
+
151
+ CNN 輸出的 shape 就會是:
152
+
153
+ [batch_size=8, out_channels=128, seq_len=8],還是每個 token 有對應一個向量(只是這向量是 CNN 抽出的新特徵)
154
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  """