Spaces:
Sleeping
Sleeping
File size: 12,777 Bytes
6709d22 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# 引入重要套件Import Library
# PyTorch 主模組,和Tensorflow很像
# 共通點:都是深度學習框架,支援建構神經網路、訓練與推論,都支援GPU加速、載入模型,和處理tensor等。
# 操作比較直覺,接近Python本身的風格,動態圖架構(每一次forward都即時計算),更容易除錯、快速迭代,在研究領域非常流行。
# re是Python內建的正則表示式(regular expression)模組,在這專案中用來"用關鍵規則篩選文字內容"。
# requests是一個非常好用的 HTTP 請求套件,能讓你從Python發送GET/POST請求,在專案中用來從Google Drive下載模型檔案(model.pth)。
# BertTokenizer:從Hugging Face的transformers套件載入一個專用的「分詞器(Tokenizer)」。
import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
import torch
import re
import easyocr
import io
import numpy as np
from PIL import Image
from huggingface_hub import hf_hub_download
from transformers import BertTokenizer
# 設定裝置(GPU 優先)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 預設模型與 tokenizer 為 None,直到首次請求才載入(延遲載入)
model = None
tokenizer = None
# ✅ 延遲載入模型與 tokenizer
def load_model_and_tokenizer():
global model, tokenizer
if os.path.exists("model.pth"):
model_path = "model.pth"
else:
model_path = hf_hub_download(repo_id="Bennie12/Bert-Lstm-Cnn-ScamDetecter", filename="model.pth")
# 匯入模型架構(避免在模組初始化階段就占用大量記憶體)
from AI_Model_architecture import BertLSTM_CNN_Classifier
"""
file_id = "19t6NlRFMc1i8bGtngRwIRtRcCmibdP9q"
url = f"https://drive.google.com/uc?export=download&id={file_id}"
if not os.path.exists(model_path): # 如果本地還沒有這個檔案 → 才下載(避免重複)
print("📥 Downloading model from Google Drive...")
r = requests.get(url) # 用requests發送GET請求到Google Drive
with open(model_path, 'wb')as f: # 把下載的檔案內容寫入到 model.pth 本地檔案
f.write(r.content)
print("✅ Model downloaded.")
else:
print("📦 Model already exists.")
"""
# 載入模型架構與參數,初始化模型架構並載入訓練權重
model = BertLSTM_CNN_Classifier()
# 這行的功能是:「從 model_path把.pth 權重檔案讀進來,載入進模型裡」。
# model.load_state_dict(...)把上面載入的權重「套進模型架構裡」
# torch.load(...)載入.pth 權重檔案,會變成一份 Python 字典
# map_location=device指定模型載入到 CPU 還是 GPU,避免報錯
model.load_state_dict(torch.load(model_path, map_location=device))
model.to(device)
# 這是PyTorch中的「推論模式」設定
# model.eval()模型處於推論狀態(關掉 Dropout 等隨機操作)
# 只要是用來「預測」而不是訓練,一定要加 .eval()!
model.eval()
# 初始化 tokenizer(不要從 build_bert_inputs 中取)
# 載入預訓練好的CKIP中文BERT分詞器
# 能把中文句子轉成 BERT 模型需要的 input 格式(input_ids, attention_mask, token_type_ids)
tokenizer = BertTokenizer.from_pretrained("ckiplab/bert-base-chinese")
return model, tokenizer
all_preds = []
all_labels = []
# 預測單一句子的分類結果(詐騙 or 正常)
# model: 訓練好的PyTorch模型
# tokenizer: 分詞器,負責把中文轉成 BERT 能處理的數值格式
# sentence: 使用者輸入的文字句子
# max_len: 限制最大輸入長度(預設 256 個 token)
def predict_single_sentence(model, tokenizer, sentence, max_len=256):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 使用 with torch.no_grad(),代表這段程式「不需要記錄梯度」
# 這樣可以加速推論並節省記憶體
with torch.no_grad():
# ----------- 文字前處理:清洗輸入句子 -----------
sentence = re.sub(r"\s+", "", sentence) # 移除所有空白字元(空格、換行等)
sentence = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", sentence)
# 保留常見中文字、英數字與標點符號,其他奇怪符號都移除
# ----------- 使用 BERT Tokenizer 將句子編碼 -----------
encoded = tokenizer(sentence,
return_tensors="pt", # 回傳 PyTorch tensor 格式(預設是 numpy 或 list)
truncation=True, # 超過最大長度就截斷
padding="max_length", # 不足最大長度則補空白(PAD token)
max_length=max_len) # 設定最大長度為 256
# 把 tokenizer 回傳的資料送進模型前,to(device)轉到指定的裝置(GPU or CPU)
input_ids = encoded["input_ids"].to(device)
attention_mask = encoded["attention_mask"].to(device)
token_type_ids = encoded["token_type_ids"].to(device)
# ----------- 模型推論:輸出詐騙的機率值 -----------
output = model(input_ids, attention_mask, token_type_ids)# 回傳的是一個機率值(float)
prob = output.item() # 從 tensor 取出純數字,例如 0.86
label = int(prob > 0.5) # 如果機率 > 0.5,標為「詐騙」(1),否則為「正常」(0)
# ----------- 根據機率進行風險分級 -----------
if prob > 0.9:
risk = "🔴 高風險(極可能是詐騙)"
elif prob > 0.5:
risk = "🟡 中風險(可疑)"
else:
risk = "🟢 低風險(正常)"
# ----------- 根據 label 判斷文字結果 -----------
pre_label ='詐騙'if label == 1 else '正常'
# ----------- 顯示推論資訊(後端終端機) -----------
print(f"\n📩 訊息內容:{sentence}")
print(f"✅ 預測結果:{'詐騙' if label == 1 else '正常'}")
print(f"📊 信心值:{round(prob*100, 2)}")
print(f"⚠️ 風險等級:{risk}")
# ----------- 回傳結果給呼叫端(通常是 API) -----------
# 組成一個 Python 字典(對應 API 的 JSON 輸出格式)
return {
"label" : pre_label, # 預測分類("詐騙" or "正常")
"prob" : prob, # 預測分類("詐騙" or "正常")
"risk" : risk # 用風險分級當作"可疑提示"放進 list(名稱為 suspicious_keywords)
}
# analyze_text(text)對應app.py第117行
# 這個函式是「對外的簡化版本」:輸入一句文字 → 回傳詐騙判定結果
# 用在主程式或 FastAPI 後端中,是整個模型預測流程的入口點
#------------ CNN ------------
def extract_suspicious_tokens_cnn(model, tokenizer, text, top_k=3):
model.eval()
model.to(device)
# 清理與編碼輸入文字
sentence = re.sub(r"\s+", "", text)
sentence = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", sentence)
encoded = tokenizer(sentence,
return_tensors="pt",
truncation=True,
padding="max_length",
max_length=128)
input_ids = encoded["input_ids"].to(device)
attention_mask = encoded["attention_mask"].to(device)
token_type_ids = encoded["token_type_ids"].to(device)
# 前向傳遞直到 CNN 輸出
with torch.no_grad():
hidden_states = model.bert(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids).last_hidden_state
lstm_out, _ = model.LSTM(hidden_states)
conv_input = lstm_out.transpose(1, 2)
conv_out = model.conv1(conv_input) # conv_out = [batch, 128, seq_len]
# 這裡會將conv_out的輸出[batch, 128, seq_len],壓縮成[seq_len],也就是轉換成bert編碼形勢的句子。
token_scores = conv_out.mean(dim=1).squeeze()
# torch.topk(token_scores, top_k)會得到分數高的token,和對應索引位置,.indices只留下索引,.cpu()把結果從GPU移到CPU(必要才能轉為 list),
# .tolist()轉化成list格式。挑出重要性最高的幾個 token 的位置索引。
topk_indices = torch.topk(token_scores, top_k).indices.cpu().tolist()
"""
tokenizer.convert_ids_to_tokens(input_ids.squeeze())將bert編碼還原成原始文字
這段input_ids = encoded["input_ids"].to(device)輸出的編碼,還原成文字
.squeeze() 去掉 batch 維度,得到 [seq_len]。
[tokens[i] for i in topk_indices if tokens[i] not in ["[PAD]", "[CLS]", "[SEP]"]]
上面的程式碼為,i為topk_indices挑出的索引,token[i]為分數最高的文字,也就是可疑的詞句。
not in 就能避免選到就能避免選到[CLS]、[SEP]、 [PAD]
[CLS] 開始符號 = 101
[SEP] 結束符號 = 102
[PAD] 補空白 = 0
"""
tokens = tokenizer.convert_ids_to_tokens(input_ids.squeeze())
suspicious_tokens = [tokens[i] for i in topk_indices if tokens[i] not in ["[PAD]", "[CLS]", "[SEP]"]]
return suspicious_tokens
#------------ Bert Attention ------------
def extract_suspicious_tokens_attention(model, tokenizer, text, top_k=3):
from transformers import BertModel # 避免重複 import
sentence = re.sub(r"\s+", "", text)
sentence = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9。,!?]", "", sentence)
encoded = tokenizer(sentence,
return_tensors="pt",
truncation=True,
padding="max_length",
max_length=128)
input_ids = encoded["input_ids"].to(device)
attention_mask = encoded["attention_mask"].to(device)
token_type_ids = encoded["token_type_ids"].to(device)
with torch.no_grad():
bert_outputs = model.bert(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
output_attentions=True)
# 取第一層第0個 head 的 attention(CLS → all tokens)
"""
attentions[0]第 0 層 attention(BERT 第 1 層),[0, 0, 0, :]取出第 0 個 batch、第 0 個 head、第 0 個 token(CLS)對所有 token 的注意力分數
"""
attention_scores = bert_outputs.attentions[0][0, 0, 0, :] # [seq_len]
topk_indices = torch.topk(attention_scores, top_k).indices.cpu().tolist()
tokens = tokenizer.convert_ids_to_tokens(input_ids.squeeze())
suspicious_tokens = [tokens[i] for i in topk_indices if tokens[i] not in ["[PAD]", "[CLS]", "[SEP]"]]
return suspicious_tokens
def analyze_text(text, explain_mode="cnn"):
model, tokenizer = load_model_and_tokenizer()
model.eval()
# 預測標籤與信心分數
result = predict_single_sentence(model, tokenizer, text)
label = result["label"]
prob = result["prob"]
risk = result["risk"]
# 根據模式擷取可疑詞
if explain_mode == "cnn":
suspicious = extract_suspicious_tokens_cnn(model, tokenizer, text)
elif explain_mode == "bert":
suspicious = extract_suspicious_tokens_attention(model, tokenizer, text)
elif explain_mode == "both":
cnn_tokens = extract_suspicious_tokens_cnn(model, tokenizer, text)
bert_tokens = extract_suspicious_tokens_attention(model, tokenizer, text)
suspicious = list(set(cnn_tokens + bert_tokens))
else:
suspicious = [risk]
return {
"status": label,
"confidence": round(prob * 100, 2),
"suspicious_keywords": [str(s) for s in suspicious]
}
def analyze_image(file_bytes, explain_mode = "cnn"):
image = Image.open(io.BytesIO(file_bytes))
image_np = np.array(image)
reader = easyocr.Reader(['ch_tra', 'en'], gpu=torch.cuda.is_available())
results = reader.readtext(image_np)
text = ' '.join([res[1] for res in results]).strip()
if not text:
return{
"status" : "無法辨識文字",
"confidence" : 0.0,
"suspicious_keywords" : ["圖片中無可辨識的中文英文"]
}
return analyze_text(text, explain_mode=explain_mode) |