Bennie12 commited on
Commit
6709d22
·
verified ·
1 Parent(s): 6f3ed2c

Upload 4 files

Browse files
Files changed (4) hide show
  1. AI_Model_architecture.py +324 -0
  2. Gradio_app.py +25 -0
  3. bert_explainer.py +267 -0
  4. requirements.txt +20 -0
AI_Model_architecture.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import os
24
+ import torch # PyTorch 主模組
25
+ import torch.nn as nn # 神經網路相關的層(例如 LSTM、Linear)
26
+ import pandas as pd
27
+ import re
28
+
29
+ from dotenv import load_dotenv
30
+ from sklearn.model_selection import train_test_split
31
+ from torch.utils.data import DataLoader, Dataset # 提供 Dataset、DataLoader 類別
32
+ from transformers import BertTokenizer # BertTokenizer把文字句子轉換成 BERT 格式的 token ID,例如 [CLS] 今天 天氣 不錯 [SEP] → [101, 1234, 5678, ...]
33
+ from sklearn.model_selection import train_test_split
34
+ from transformers import BertModel
35
+
36
+ """
37
+ # ------------------- 載入 .env 環境變數 -------------------
38
+ load_dotenv()
39
+ base_dir = os.getenv("DATA_DIR", "./data") # 如果沒設環境變數就預設用 ./data
40
+
41
+ # ------------------- 使用相對路徑找 CSV -------------------
42
+ #,os.path.join(base_dir, "NorANDScamInfo_data1.csv"),os.path.join(base_dir, "ScamInfo_data1.csv"),os.path.join(base_dir, "NormalInfo_data1.csv")
43
+ #如有需要訓練複數筆資料可以使用這個方法csv_files = [os.path.join(base_dir, "檔案名稱1.csv"),os.path.join(base_dir, "檔案名稱2.csv")]
44
+ #程式碼一至131行
45
+
46
+ # GPU 記憶體限制(可選)
47
+ # os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:16"
48
+
49
+ #資料前處理
50
+ class BertPreprocessor:
51
+ def __init__(self, tokenizer_name="ckiplab/bert-base-chinese", max_len=128):
52
+ self.tokenizer = BertTokenizer.from_pretrained(tokenizer_name)
53
+ self.max_len = max_len
54
+
55
+ def load_and_clean(self, filepath):
56
+ #載入 CSV 並清理 message 欄位。
57
+ df = pd.read_csv(filepath)
58
+ df = df.dropna().drop_duplicates().reset_index(drop=True)
59
+ # 文字清理:移除空白、保留中文英數與標點
60
+ df["message"] = df["message"].astype(str)
61
+ df["message"] = df["message"].apply(lambda text: re.sub(r"\s+", "", text))
62
+ df["message"] = df["message"].apply(lambda text: re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", text))
63
+ return df[["message", "label"]] # 保留必要欄位
64
+
65
+ def encode(self, messages):
66
+ #使用 HuggingFace BERT Tokenizer 將訊息編碼成Bert模型輸入格式。
67
+ return self.tokenizer(
68
+ list(messages),
69
+ return_tensors="pt",
70
+ truncation=True,
71
+ padding="max_length",
72
+ max_length=self.max_len
73
+ )
74
+ #自動做資料前處理
75
+ def build_bert_inputs(files):
76
+ #將正常與詐騙資料分別指定 label,統一清理、編碼,回傳模型可用的 input tensors 與 labels。
77
+ processor = BertPreprocessor()
78
+ dfs = []
79
+ # 合併正常 + 詐騙檔案清單
80
+ all_files = files
81
+
82
+ for filepath in all_files:
83
+ df = processor.load_and_clean(filepath)
84
+ dfs.append(df)
85
+
86
+ # 合併所有資料。在資料清理過程中dropna():刪除有空值的列,drop_duplicates():刪除重複列,filter()或df[...]做條件過濾,concat():將多個 DataFrame合併
87
+ # 這些操作不會自動重排索引,造成索引亂掉。
88
+ # 合併後統一編號(常見於多筆資料合併)all_df = pd.concat(dfs, 關鍵-->ignore_index=True)
89
+ all_df = pd.concat(dfs, ignore_index=True)
90
+ print(f"✅ 已讀入 {len(all_df)} 筆資料")
91
+ print(all_df["label"].value_counts())
92
+ #製作 train/val 資料集
93
+ train_texts, val_texts, train_labels, val_labels = train_test_split(
94
+ all_df["message"], all_df["label"],
95
+ stratify=all_df["label"],
96
+ test_size=0.2,
97
+ random_state=25,
98
+ shuffle=True
99
+ )
100
+ # 進行 BERT tokenizer 編碼
101
+ train_inputs = processor.encode(train_texts)
102
+ val_inputs = processor.encode(val_texts)
103
+
104
+ return train_inputs, train_labels, val_inputs, val_labels, processor
105
+
106
+
107
+ #定義 PyTorch Dataset 類別。ScamDataset 繼承自 torch.utils.data.Dataset
108
+ #將 BERT 輸出的 token 與對應標籤封裝成 PyTorch 能使用的格式
109
+ class ScamDataset(Dataset):
110
+ def __init__(self, inputs, labels):
111
+ self.input_ids = inputs["input_ids"] # input_ids:句子的 token ID;attention_mask:注意力遮罩(0 = padding)
112
+ self.attention_mask = inputs["attention_mask"] # token_type_ids:句子的 segment 區分
113
+ self.token_type_ids = inputs["token_type_ids"] # torch.tensor(x, dtype=...)將資料(x)轉為Tensor���標準做法。
114
+ self.labels = torch.tensor(labels.values, dtype=torch.float32) # x可以是 list、NumPy array、pandas series...
115
+ # dtypefloat32:浮點數(常用於 回歸 或 BCELoss 二分類);long:整數(常用於 多分類 搭配 CrossEntropyLoss)。labels.values → 轉為 NumPy array
116
+
117
+ def __len__(self): # 告訴 PyTorch 這個 Dataset 有幾筆資料
118
+ return len(self.labels) # 給 len(dataset) 或 for i in range(len(dataset)) 用的
119
+
120
+ def __getitem__(self, idx): #每次調用 __getitem__() 回傳一筆 {input_ids, attention_mask, token_type_ids, labels}
121
+ return { #DataLoader 每次會呼叫這個方法多次來抓一個 batch 的資料
122
+ "input_ids":self.input_ids[idx],
123
+ "attention_mask":self.attention_mask[idx],
124
+ "token_type_ids":self.token_type_ids[idx],
125
+ "labels":self.labels[idx]
126
+ }
127
+
128
+ # 這樣可以同時處理 scam 和 normal 資料,不用重複寫清理與 token 處理
129
+ if __name__ == "__main__":
130
+ csv_files = [os.path.join(base_dir, "NorANDScamInfo_data3k.csv")]
131
+ train_inputs, train_labels, val_inputs, val_labels, processor = build_bert_inputs(csv_files)
132
+
133
+ train_dataset = ScamDataset(train_inputs, train_labels)
134
+ val_dataset = ScamDataset(val_inputs, val_labels)
135
+
136
+ # batch_size每次送進模型的是 8 筆資料(而不是一筆一筆)
137
+ # 每次從 Dataset 中抓一批(batch)資料出來
138
+ train_loader = DataLoader(train_dataset, batch_size=8)
139
+ val_loader = DataLoader(val_dataset, batch_size=8)
140
+ """
141
+
142
+
143
+ """
144
+ class BertLSTM_CNN_Classifier(nn.Module)表示:你定義了一個子類別,
145
+ 繼承自 PyTorch 的基礎模型類別 nn.Module。
146
+
147
+ 若你在 __init__() 裡沒有呼叫 super().__init__(),
148
+ 那麼父類別 nn.Module 的初始化邏輯(包含重要功能)就不會被執行,
149
+ 導致整個模型運作異常或錯誤。
150
+ """
151
+
152
+ # nn.Module是PyTorch所有神經網路模型的基礎類別,nn.Module 是 PyTorch 所有神經網路模型的基礎類別
153
+ class BertLSTM_CNN_Classifier(nn.Module):
154
+
155
+ def __init__(self, hidden_dim=128, num_layers=1, dropout=0.3):
156
+
157
+ # super()是Python提供的一個方法,用來呼叫「父類別的版本」的方法。
158
+ # 呼叫:super().__init__()讓父類別(nn.Module)裡面那些功能、屬性都被正確初始化。
159
+ # 沒super().__init__(),這些都不會正確運作,模型會壞掉。
160
+ # super() 就是 Python 提供給「子類別呼叫父類別方法」的方式
161
+ super().__init__()
162
+
163
+ # 載入中文預訓練的 BERT 模型,輸入為句子token IDs,輸出為每個 token 的向量,大小為 [batch, seq_len, 768]。
164
+ self.bert = BertModel.from_pretrained("ckiplab/bert-base-chinese") # 這是引入hugging face中的tranceformat
165
+
166
+ # 接收BERT的輸出(768 維向量),進行雙向LSTM(BiLSTM)建模,輸出為 [batch, seq_len, hidden_dim*2],例如 [batch, seq_len, 256]
167
+ """
168
+ LSTM 接收每個token的768維向量(來自 BERT)作為輸入,
169
+ 透過每個方向的LSTM壓縮成128維的語意向量。
170
+ 由於是雙向LSTM,會同時從左到右(前向)和右到左(後向)各做一次,
171
+ 最後將兩個方向的輸出合併為256維向量(128×2)。
172
+ 每次處理一個 batch(例如 8 句話),一次走完整個時間序列。
173
+ """
174
+ self.LSTM = nn.LSTM(input_size=768,
175
+ hidden_size=hidden_dim,
176
+ num_layers=num_layers,
177
+ batch_first=True,
178
+ bidirectional=True)
179
+
180
+ # CNN 模組:接在 LSTM 後的輸出上。將LSTM的輸出轉成卷積層格式,適用於Conv1D,CNN可學習位置不變的局部特徵。
181
+ self.conv1 = nn.Conv1d(in_channels=hidden_dim*2,
182
+ out_channels=128,
183
+ kernel_size=3, # 這裡kernel_size=3 為 3-gram 特徵
184
+ padding=1)
185
+
186
+ self.dropout = nn.Dropout(dropout) # 隨機將部分神經元設為 0,用來防止 overfitting。
187
+
188
+ self.global_maxpool = nn.AdaptiveAvgPool1d(1) #將一整句話的特徵濃縮成一個固定大小的句子表示向量
189
+
190
+ # 將CNN輸出的128維特徵向量輸出為一個「機率值」(詐騙或非詐騙)。
191
+ self.classifier = nn.Linear(128,1)
192
+
193
+ def forward(self, input_ids, attention_mask, token_type_ids):
194
+ #BERT 編碼
195
+ outputs = self.bert(input_ids=input_ids,
196
+ attention_mask=attention_mask,
197
+ token_type_ids=token_type_ids)
198
+ #.last_hidden_state是BertModel.from_pretrained(...)內部的key,會輸出 [batch, seq_len, 768]
199
+ hidden_states = outputs.last_hidden_state
200
+
201
+ # 送入 BiLSTM
202
+ # transpose(1, 2) 的用途是:讓 LSTM 輸出的資料形狀��合 CNN 所要求的格式
203
+ # 假設你原本 LSTM 輸出是: [batch_size, seq_len, hidden_dim*2] = [8, 128, 256]
204
+ # 但CNN(Conv1d)的輸入格式需要是:[batch_size, in_channels, seq_len] = [8, 256, 128]
205
+ # 因此你需要做:.transpose(1, 2)把 seq_len 和 hidden_dim*2 調換
206
+ LSTM_out, _ = self.LSTM(hidden_states) # [batch, seq_len, hidden_dim*2]
207
+ LSTM_out = LSTM_out.transpose(1, 2) # [batch, hidden_dim*2, seq_len]
208
+
209
+ # 卷積 + Dropout
210
+ x = self.conv1(LSTM_out) # [batch, 128, seq_len]
211
+ x = self.dropout(x)
212
+
213
+ #全局池化
214
+ # .squeeze(dim) 的作用是:把某個「維度大小為 1」的維度刪掉
215
+ # x = self.global_maxpool(x).squeeze(2) # 輸出是 [batch, 128, 1]
216
+ # 不 .squeeze(2),你會得到 shape 為 [batch, 128, 1],不方便後面接 Linear。
217
+ # .squeeze(2)=拿掉第 2 維(數值是 1) → 讓形狀變成 [batch, 128]
218
+ x = self.global_maxpool(x).squeeze(2) # [batch, 128]
219
+
220
+ #分類 & Sigmoid 機率輸出
221
+ logits = self.classifier(x)
222
+
223
+ #.sigmoid() → 把 logits 轉成 0~1 的機率.squeeze() → 變成一維 [batch] 長度的機率 list
224
+ """例如:
225
+ logits = [[0.92], [0.05], [0.88], [0.41], ..., [0.17]]
226
+ → sigmoid → [[0.715], [0.512], ...]
227
+ → squeeze → [0.715, 0.512, ...]
228
+ """
229
+ return torch.sigmoid(logits).squeeze() # 最後輸出是一個值介於 0 ~ 1 之間,代表「為詐騙訊息的機率」。
230
+
231
+
232
+ """
233
+ # 設定 GPU 裝置
234
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
235
+
236
+ # 初始化模型
237
+ model = BertLSTM_CNN_Classifier().to(device)
238
+ # 定義 optimizer 和損失函數
239
+ optimizer = torch.optim.Adam(model.parameters(),lr=2e-5)
240
+ criterion = nn.BCELoss()
241
+
242
+ # 本機訓練迴圈,要訓練再取消註解,否則在線上版本一律處於註解狀態
243
+
244
+ if __name__ == "__main__": # 只有當我「直接執行這個檔案」時,才執行以下訓練程式(不是被別人 import 使用時)。
245
+ if os.path.exists("model.pth"):
246
+ print("✅ 已找到 model.pth,載入模型跳過訓練")
247
+ model.load_state_dict(torch.load("model.pth", map_location=device))
248
+ else:
249
+ print("🚀 未找到 model.pth,開始訓練模型...")
250
+ num_epochs = 15 # batch_size設定在train_loader和test_loader那
251
+ for epoch in range(num_epochs):
252
+ model.train() # 從nn.Module繼承的方法。將模型設為「訓練模式」,有些層(像 Dropout 或 BatchNorm)會啟用訓練行為。
253
+ total_loss = 0.0
254
+ for batch in train_loader:
255
+
256
+ # 清理舊梯度,以免累加。為甚麼要?因為PyTorch 預設每次呼叫 .backward() 都會「累加」梯度(不會自動清掉)
257
+ # 沒 .zero_grad(),梯度會越累積越多,模型會亂掉。
258
+ optimizer.zero_grad()
259
+
260
+ input_ids = batch["input_ids"].to(device)
261
+ attention_mask = batch["attention_mask"].to(device)
262
+ token_type_ids = batch["token_type_ids"].to(device)
263
+ labels = batch["labels"].to(device)
264
+ outputs = model(input_ids, attention_mask, token_type_ids)
265
+
266
+ loss = criterion(outputs, labels) # 比較 預測結果 outputs(Sigmoid 的機率)和 真實答案 labels
267
+
268
+ # 用鏈式法則(Chain Rule)計算每一層「參數對 loss 的影響」,也就是梯度
269
+ # PyTorch 利用自動微分(autograd)幫你計算整個計算圖的偏導數,然後存在每一層的 .grad 裡。
270
+ loss.backward()
271
+
272
+ # 用 .grad 中的梯度資訊根
273
+ # 據學習率和優化器的規則
274
+ # 改變每一個參數的值,以讓下一次預測更接近真實
275
+ optimizer.step()
276
+
277
+ # loss 是一個 tensor(需要 backward);.item() 把它轉成 Python 的純數字(float)
278
+ total_loss += loss.item()
279
+ print(f"[Epoch{epoch+1}]Training Loss:{total_loss:.4f}")
280
+ torch.save(model.state_dict(), "model.pth")# 儲存模型權重
281
+ print("✅ 模型訓練完成並儲存為 model.pth")
282
+ """
283
+
284
+ """
285
+ 整個模型中每一個文字(token)始終是一個向量,隨著層數不同,這個向量代表的意義會更高階、更語意、更抽象。
286
+ 在整個 BERT + LSTM + CNN 模型的流程中,「每一個文字(token)」都會被表示成一個「向量」來進行後續的計算與學習。
287
+ 今天我輸入一個句子:"早安你好,吃飯沒"
288
+ BERT 的輸入包含三個部分:input_ids、attention_mask、token_type_ids,
289
+ 這些是 BERT 所需的格式。BERT 會將句子中每個 token 編碼為一個 768 維的語意向量,
290
+
291
+ 進入 BERT → 每個 token 變成語意向量:
292
+ BERT 輸出每個字為一個 768 維的語意向量
293
+ 「早」 → [0.23, -0.11, ..., 0.45] 長度為 768
294
+ 「安」 → [0.05, 0.33, ..., -0.12] 一樣 768
295
+ ...
296
+ batch size 是 8,句子長度是 8,輸出 shape 為:
297
+ [batch_size=8, seq_len=8, hidden_size=768]
298
+
299
+ 接下來這些向量會輸入到 LSTM,LSTM不會改變「一個token是一個向量」的概念,而是重新表示每個token的語境向量。
300
+ 把每個原本 768 維的 token 壓縮成 hidden_size=128,雙向 LSTM → 拼接 → 每個 token 成為 256 維向量:
301
+
302
+ input_size=768 是從 BERT 接收的向量維度
303
+ hidden_size=128 表示每個方向的 LSTM 會把 token 壓縮為 128 維語意向量
304
+ num_layers=1 表示只堆疊 1 層 LSTM
305
+ bidirectional=True 表示是雙向
306
+
307
+ LSTM,除了從左讀到右,也會從右讀到左,兩個方向的輸出會合併(拼接),變成:
308
+ [batch_size=8, seq_len=8, hidden_size=256] # 因為128*2
309
+
310
+ 接下來進入 CNN,CNN 仍然以「一個向量代表一個字」的形式處理:
311
+
312
+ in_channels=256(因為 LSTM 是雙向輸出)
313
+
314
+ out_channels=128 表示學習出 128 個濾波器,每個濾波器專門抓一種 n-gram(例如「早安你」),每個「片段」的結果輸出為 128 維特徵
315
+
316
+ kernel_size=3 表示每個濾波器看 3 個連續 token(像是一個 3-gram)或,把相鄰的 3 個字(各為 256 維)一起掃描
317
+
318
+ padding=1 為了保留輸出序列長度和輸入相同,避免邊界資訊被捨棄
319
+
320
+ CNN 輸出的 shape 就會是:
321
+
322
+ [batch_size=8, out_channels=128, seq_len=8],還是每個 token 有對應一個向量(只是這向量是 CNN 抽出的新特徵)
323
+
324
+ """
Gradio_app.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from bert_explainer import analyze_text
3
+
4
+ def predict_fn(text, mode):
5
+ result = analyze_text(text=text, explain_mode=mode)
6
+ status = result['status']
7
+ confidence = f"{result['confidence']}%"
8
+ keywords = ', '.join(result['suspicious_keywords'])
9
+ return status, confidence, keywords
10
+
11
+ iface =gr.Interface(
12
+ fn=predict_fn,
13
+ inputs = [
14
+ gr.TextArea(label="輸入訊息"),
15
+ gr.Radio(choices=["cnn", "bert", "both"], label="分析模式", value="cnn")
16
+ ],
17
+ outputs = [
18
+ gr.Textbox(label = "判斷結果"),
19
+ gr.Textbox(label = "可疑分數"),
20
+ gr.Textbox(label = "可疑詞彙")
21
+ ],
22
+ title= "預判詐騙訊息",
23
+ description="輸入訊息,AI 將判定是否為詐騙並標記可疑詞 "
24
+ )
25
+ iface.launch()
bert_explainer.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # 引入重要套件Import Library
3
+ # PyTorch 主模組,和Tensorflow很像
4
+ # 共通點:都是深度學習框架,支援建構神經網路、訓練與推論,都支援GPU加速、載入模型,和處理tensor等。
5
+ # 操作比較直覺,接近Python本身的風格,動態圖架構(每一次forward都即時計算),更容易除錯、快速迭代,在研究領域非常流行。
6
+ # re是Python內建的正則表示式(regular expression)模組,在這專案中用來"用關鍵規則篩選文字內容"。
7
+ # requests是一個非常好用的 HTTP 請求套件,能讓你從Python發送GET/POST請求,在專案中用來從Google Drive下載模型檔案(model.pth)。
8
+ # BertTokenizer:從Hugging Face的transformers套件載入一個專用的「分詞器(Tokenizer)」。
9
+ import os
10
+ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
11
+
12
+
13
+ import torch
14
+ import re
15
+ import easyocr
16
+ import io
17
+ import numpy as np
18
+
19
+ from PIL import Image
20
+ from huggingface_hub import hf_hub_download
21
+ from transformers import BertTokenizer
22
+
23
+
24
+
25
+
26
+ # 設定裝置(GPU 優先)
27
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
28
+ # 預設模型與 tokenizer 為 None,直到首次請求才載入(延遲載入)
29
+ model = None
30
+ tokenizer = None
31
+ # ✅ 延遲載入模型與 tokenizer
32
+ def load_model_and_tokenizer():
33
+ global model, tokenizer
34
+ if os.path.exists("model.pth"):
35
+ model_path = "model.pth"
36
+ else:
37
+ model_path = hf_hub_download(repo_id="Bennie12/Bert-Lstm-Cnn-ScamDetecter", filename="model.pth")
38
+ # 匯入模型架構(避免在模組初始化階段就占用大量記憶體)
39
+ from AI_Model_architecture import BertLSTM_CNN_Classifier
40
+ """
41
+ file_id = "19t6NlRFMc1i8bGtngRwIRtRcCmibdP9q"
42
+
43
+ url = f"https://drive.google.com/uc?export=download&id={file_id}"
44
+ if not os.path.exists(model_path): # 如果本地還沒有這個檔案 → 才下載(避免重複)
45
+ print("📥 Downloading model from Google Drive...")
46
+ r = requests.get(url) # 用requests發送GET請求到Google Drive
47
+ with open(model_path, 'wb')as f: # 把下載的檔案內容寫入到 model.pth 本地檔案
48
+ f.write(r.content)
49
+ print("✅ Model downloaded.")
50
+ else:
51
+ print("📦 Model already exists.")
52
+ """
53
+ # 載入模型架構與參數,初始化模型架構並載入訓練權重
54
+ model = BertLSTM_CNN_Classifier()
55
+
56
+ # 這行的功能是:「從 model_path把.pth 權重檔案讀進來,載入進模型裡」。
57
+ # model.load_state_dict(...)把上面載入的權重「套進模型架構裡」
58
+ # torch.load(...)載入.pth 權重檔案,會變成一份 Python 字典
59
+ # map_location=device指定模型載入到 CPU 還是 GPU,避免報錯
60
+ model.load_state_dict(torch.load(model_path, map_location=device))
61
+
62
+ model.to(device)
63
+
64
+ # 這是PyTorch中的「推論模式」設定
65
+ # model.eval()模型處於推論狀態(關掉 Dropout 等隨機操作)
66
+ # 只要是用來「預測」而不是訓練,一定要加 .eval()!
67
+ model.eval()
68
+
69
+ # 初始化 tokenizer(不要從 build_bert_inputs 中取)
70
+ # 載入預訓練好的CKIP中文BERT分詞器
71
+ # 能把中文句子轉成 BERT 模型需要的 input 格式(input_ids, attention_mask, token_type_ids)
72
+ tokenizer = BertTokenizer.from_pretrained("ckiplab/bert-base-chinese")
73
+
74
+ return model, tokenizer
75
+
76
+ all_preds = []
77
+ all_labels = []
78
+
79
+ # 預測單一句子的分類結果(詐騙 or 正常)
80
+ # model: 訓練好的PyTorch模型
81
+ # tokenizer: 分詞器,負責把中文轉成 BERT 能處理的數值格式
82
+ # sentence: 使用者輸入的文字句子
83
+ # max_len: 限制最大輸入長度(預設 256 個 token)
84
+ def predict_single_sentence(model, tokenizer, sentence, max_len=256):
85
+
86
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
87
+
88
+ # 使用 with torch.no_grad(),代表這段程式「不需要記錄梯度」
89
+ # 這樣可以加速推論並節省記憶體
90
+ with torch.no_grad():
91
+ # ----------- 文字前處理:清洗輸入句子 -----------
92
+ sentence = re.sub(r"\s+", "", sentence) # 移除所有空白字元(空格、換行等)
93
+ sentence = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", sentence)
94
+ # 保留常見中文字、英數字與標點符號,其他奇怪符號都移除
95
+ # ----------- 使用 BERT Tokenizer 將句子編碼 -----------
96
+ encoded = tokenizer(sentence,
97
+ return_tensors="pt", # 回傳 PyTorch tensor 格式(預設是 numpy 或 list)
98
+ truncation=True, # 超過最大長度就截斷
99
+ padding="max_length", # 不足最大長度則補空白(PAD token)
100
+ max_length=max_len) # 設定最大長��為 256
101
+ # 把 tokenizer 回傳的資料送進模型前,to(device)轉到指定的裝置(GPU or CPU)
102
+ input_ids = encoded["input_ids"].to(device)
103
+ attention_mask = encoded["attention_mask"].to(device)
104
+ token_type_ids = encoded["token_type_ids"].to(device)
105
+ # ----------- 模型推論:輸出詐騙的機率值 -----------
106
+ output = model(input_ids, attention_mask, token_type_ids)# 回傳的是一個機率值(float)
107
+ prob = output.item() # 從 tensor 取出純數字,例如 0.86
108
+ label = int(prob > 0.5) # 如果機率 > 0.5,標為「詐騙」(1),否則為「正常」(0)
109
+ # ----------- 根據機率進行風險分級 -----------
110
+ if prob > 0.9:
111
+ risk = "🔴 高風險(極可能是詐騙)"
112
+ elif prob > 0.5:
113
+ risk = "🟡 中風險(可疑)"
114
+ else:
115
+ risk = "🟢 低風險(正常)"
116
+ # ----------- 根據 label 判斷文字結果 -----------
117
+ pre_label ='詐騙'if label == 1 else '正常'
118
+ # ----------- 顯示推論資訊(後端終端機) -----------
119
+ print(f"\n📩 訊息內容:{sentence}")
120
+ print(f"✅ 預測結果:{'詐騙' if label == 1 else '正常'}")
121
+ print(f"📊 信心值:{round(prob*100, 2)}")
122
+ print(f"⚠️ 風險等級:{risk}")
123
+ # ----------- 回傳結果給呼叫端(通常是 API) -----------
124
+ # 組成一個 Python 字典(對應 API 的 JSON 輸出格式)
125
+ return {
126
+ "label" : pre_label, # 預測分類("詐騙" or "正常")
127
+ "prob" : prob, # 預測分類("詐騙" or "正常")
128
+ "risk" : risk # 用風險分級當作"可疑提示"放進 list(名稱為 suspicious_keywords)
129
+ }
130
+
131
+ # analyze_text(text)對應app.py第117行
132
+ # 這個函式是「對外的簡化版本」:輸入一句文字 → 回傳詐騙判定結果
133
+ # 用在主程式或 FastAPI 後端中,是整個模型預測流程的入口點
134
+
135
+
136
+ #------------ CNN ------------
137
+ def extract_suspicious_tokens_cnn(model, tokenizer, text, top_k=3):
138
+ model.eval()
139
+ model.to(device)
140
+
141
+ # 清理與編碼輸入文字
142
+ sentence = re.sub(r"\s+", "", text)
143
+ sentence = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", sentence)
144
+
145
+ encoded = tokenizer(sentence,
146
+ return_tensors="pt",
147
+ truncation=True,
148
+ padding="max_length",
149
+ max_length=128)
150
+
151
+ input_ids = encoded["input_ids"].to(device)
152
+ attention_mask = encoded["attention_mask"].to(device)
153
+ token_type_ids = encoded["token_type_ids"].to(device)
154
+
155
+ # 前向傳遞直到 CNN 輸出
156
+ with torch.no_grad():
157
+ hidden_states = model.bert(input_ids=input_ids,
158
+ attention_mask=attention_mask,
159
+ token_type_ids=token_type_ids).last_hidden_state
160
+ lstm_out, _ = model.LSTM(hidden_states)
161
+ conv_input = lstm_out.transpose(1, 2)
162
+ conv_out = model.conv1(conv_input) # conv_out = [batch, 128, seq_len]
163
+
164
+ # 這裡會將conv_out的輸出[batch, 128, seq_len],壓縮成[seq_len],也就是轉換成bert編碼形勢的句子。
165
+ token_scores = conv_out.mean(dim=1).squeeze()
166
+
167
+ # torch.topk(token_scores, top_k)會得到分數高的token,和對應索引位置,.indices只留下索引,.cpu()把結果從GPU移到CPU(必要才能轉為 list),
168
+ # .tolist()轉化成list格式。挑出重要性最高的幾個 token 的位置索引。
169
+ topk_indices = torch.topk(token_scores, top_k).indices.cpu().tolist()
170
+
171
+ """
172
+ tokenizer.convert_ids_to_tokens(input_ids.squeeze())將bert編碼還原成原始文字
173
+ 這段input_ids = encoded["input_ids"].to(device)輸出的編碼,還原成文字
174
+ .squeeze() 去掉 batch 維度,得到 [seq_len]。
175
+ [tokens[i] for i in topk_indices if tokens[i] not in ["[PAD]", "[CLS]", "[SEP]"]]
176
+ 上面的程式碼為,i為topk_indices挑出的索引,token[i]為分數最高的文字,也就是可疑的詞句。
177
+ not in 就能避免選到就能避免選到[CLS]、[SEP]、 [PAD]
178
+ [CLS] 開始符號 = 101
179
+ [SEP] 結束符號 = 102
180
+ [PAD] 補空白 = 0
181
+ """
182
+ tokens = tokenizer.convert_ids_to_tokens(input_ids.squeeze())
183
+ suspicious_tokens = [tokens[i] for i in topk_indices if tokens[i] not in ["[PAD]", "[CLS]", "[SEP]"]]
184
+
185
+ return suspicious_tokens
186
+
187
+
188
+ #------------ Bert Attention ------------
189
+ def extract_suspicious_tokens_attention(model, tokenizer, text, top_k=3):
190
+ from transformers import BertModel # 避免重複 import
191
+
192
+ sentence = re.sub(r"\s+", "", text)
193
+ sentence = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", sentence)
194
+
195
+ encoded = tokenizer(sentence,
196
+ return_tensors="pt",
197
+ truncation=True,
198
+ padding="max_length",
199
+ max_length=128)
200
+
201
+ input_ids = encoded["input_ids"].to(device)
202
+ attention_mask = encoded["attention_mask"].to(device)
203
+ token_type_ids = encoded["token_type_ids"].to(device)
204
+
205
+ with torch.no_grad():
206
+ bert_outputs = model.bert(input_ids=input_ids,
207
+ attention_mask=attention_mask,
208
+ token_type_ids=token_type_ids,
209
+ output_attentions=True)
210
+ # 取第一層第0個 head 的 attention(CLS → all tokens)
211
+ """
212
+ attentions[0]第 0 層 attention(BERT 第 1 層),[0, 0, 0, :]取出第 0 個 batch、第 0 個 head、第 0 個 token(CLS)對所有 token 的注意力分數
213
+
214
+ """
215
+ attention_scores = bert_outputs.attentions[0][0, 0, 0, :] # [seq_len]
216
+
217
+ topk_indices = torch.topk(attention_scores, top_k).indices.cpu().tolist()
218
+
219
+ tokens = tokenizer.convert_ids_to_tokens(input_ids.squeeze())
220
+ suspicious_tokens = [tokens[i] for i in topk_indices if tokens[i] not in ["[PAD]", "[CLS]", "[SEP]"]]
221
+
222
+ return suspicious_tokens
223
+
224
+
225
+
226
+ def analyze_text(text, explain_mode="cnn"):
227
+ model, tokenizer = load_model_and_tokenizer()
228
+ model.eval()
229
+
230
+ # 預測標籤與信心分數
231
+ result = predict_single_sentence(model, tokenizer, text)
232
+ label = result["label"]
233
+ prob = result["prob"]
234
+ risk = result["risk"]
235
+ # 根據模式擷取可疑詞
236
+ if explain_mode == "cnn":
237
+ suspicious = extract_suspicious_tokens_cnn(model, tokenizer, text)
238
+ elif explain_mode == "bert":
239
+ suspicious = extract_suspicious_tokens_attention(model, tokenizer, text)
240
+ elif explain_mode == "both":
241
+ cnn_tokens = extract_suspicious_tokens_cnn(model, tokenizer, text)
242
+ bert_tokens = extract_suspicious_tokens_attention(model, tokenizer, text)
243
+ suspicious = list(set(cnn_tokens + bert_tokens))
244
+ else:
245
+ suspicious = [risk]
246
+
247
+ return {
248
+ "status": label,
249
+ "confidence": round(prob * 100, 2),
250
+ "suspicious_keywords": [str(s) for s in suspicious]
251
+ }
252
+
253
+ def analyze_image(file_bytes, explain_mode = "cnn"):
254
+ image = Image.open(io.BytesIO(file_bytes))
255
+ image_np = np.array(image)
256
+ reader = easyocr.Reader(['ch_tra', 'en'], gpu=torch.cuda.is_available())
257
+ results = reader.readtext(image_np)
258
+
259
+ text = ' '.join([res[1] for res in results]).strip()
260
+
261
+ if not text:
262
+ return{
263
+ "status" : "無法辨識文字",
264
+ "confidence" : 0.0,
265
+ "suspicious_keywords" : ["圖片中無可辨識的中文英文"]
266
+ }
267
+ return analyze_text(text, explain_mode=explain_mode)
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi#==0.104.1
2
+ uvicorn#==0.24.0
3
+ python-multipart#==0.0.6
4
+ firebase-admin#==6.2.0
5
+ pandas#==2.1.3
6
+ python-jose#==3.3.0
7
+ pydantic#==2.5.2
8
+ python-dotenv#==1.0.0
9
+ transformers
10
+ torch
11
+ requests
12
+ scikit-learn#==1.3.2
13
+ python-dotenv
14
+ aiofiles
15
+ easyocr
16
+ Pillow
17
+ numpy
18
+ gradio
19
+
20
+ huggingface_hub