Perth0603 commited on
Commit
ab133d5
·
verified ·
1 Parent(s): 4ecc7b5

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -223
app.py CHANGED
@@ -1,223 +1,54 @@
1
- import os
2
- from typing import List, Optional, Dict, Any
3
-
4
- import torch
5
- from fastapi import FastAPI
6
- from fastapi.middleware.cors import CORSMiddleware
7
- from fastapi.responses import JSONResponse
8
- from pydantic import BaseModel, Field
9
- from transformers import (
10
- AutoTokenizer,
11
- AutoModelForSequenceClassification,
12
- AutoConfig,
13
- )
14
-
15
- # -----------------------------
16
- # Runtime & cache configuration
17
- # -----------------------------
18
- # 兼容容器/无写权限环境:把 HF/torch 缓存指到可写目录
19
- os.environ.setdefault("HOME", "/data")
20
- os.environ.setdefault("XDG_CACHE_HOME", "/data/.cache")
21
- os.environ.setdefault("HF_HOME", "/data/.cache")
22
- os.environ.setdefault("TRANSFORMERS_CACHE", "/data/.cache")
23
- os.environ.setdefault("TORCH_HOME", "/data/.cache")
24
-
25
- CACHE_DIR = os.environ.get("HF_CACHE_DIR", "/data/.cache")
26
- os.makedirs(CACHE_DIR, exist_ok=True)
27
-
28
- # -----------------------------
29
- # Model configuration
30
- # -----------------------------
31
- MODEL_ID = os.environ.get("MODEL_ID", "Perth0603/phishing-email-mobilebert")
32
- MAX_LENGTH = int(os.environ.get("MAX_LENGTH", "256")) # 和你最初示例保持一致的默认 256
33
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
34
- DTYPE_FP16 = bool(int(os.environ.get("FP16", "0"))) and torch.cuda.is_available()
35
-
36
- # 限制 CPU 线程数(避免过度并发导致抖动)
37
- torch.set_num_threads(int(os.environ.get("TORCH_NUM_THREADS", "1")))
38
-
39
- # -----------------------------
40
- # FastAPI app
41
- # -----------------------------
42
- app = FastAPI(title="Text Classifier API", version="2.0.0")
43
-
44
- # 可按需放开跨域
45
- app.add_middleware(
46
- CORSMiddleware,
47
- allow_origins=[os.environ.get("CORS_ALLOW_ORIGINS", "*")],
48
- allow_credentials=True,
49
- allow_methods=["*"],
50
- allow_headers=["*"],
51
- )
52
-
53
- # -----------------------------
54
- # Pydantic schemas
55
- # -----------------------------
56
- class PredictRequest(BaseModel):
57
- texts: List[str] = Field(default_factory=list, description="要预测的文本列表")
58
- # 可选:如果你只想返回指定阈值的二分类标签,可以传 threshold
59
- threshold: Optional[float] = Field(
60
- default=None,
61
- ge=0.0,
62
- le=1.0,
63
- description="二分类时用于将正类概率转为标签的阈值(不传则使用 argmax)",
64
- )
65
-
66
-
67
- class PredictResult(BaseModel):
68
- label: int
69
- score: float # 按你示例,返回的是类别1(正类)的概率
70
-
71
-
72
- class PredictResponse(BaseModel):
73
- results: List[PredictResult]
74
-
75
-
76
- # -----------------------------
77
- # Lazy singletons
78
- # -----------------------------
79
- _tokenizer = None
80
- _model = None
81
- _id2label: Dict[int, str] = {}
82
- _label2id: Dict[str, int] = {}
83
-
84
-
85
- def _load_model() -> None:
86
- """Lazy load model/tokenizer/config; warmup once."""
87
- global _tokenizer, _model, _id2label, _label2id
88
- if _tokenizer is not None and _model is not None:
89
- return
90
-
91
- config = AutoConfig.from_pretrained(MODEL_ID, cache_dir=CACHE_DIR)
92
- _id2label = {int(k): v for k, v in getattr(config, "id2label", {}).items()} or {
93
- 0: "NEGATIVE",
94
- 1: "POSITIVE",
95
- }
96
- _label2id = {v: k for k, v in _id2label.items()}
97
-
98
- _tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, cache_dir=CACHE_DIR)
99
-
100
- # dtype 仅在 GPU 下且你明确开启 FP16 时使用
101
- dtype = torch.float16 if (DTYPE_FP16 and DEVICE == "cuda") else None
102
- _model = AutoModelForSequenceClassification.from_pretrained(
103
- MODEL_ID, cache_dir=CACHE_DIR, torch_dtype=dtype
104
- ).to(DEVICE)
105
-
106
- # 预热
107
- with torch.no_grad():
108
- dummy = _tokenizer(["warm up"], return_tensors="pt").to(DEVICE)
109
- _ = _model(**dummy).logits
110
-
111
-
112
- # -----------------------------
113
- # Utility
114
- # -----------------------------
115
- def _to_int_label_from_argmax(idx: int) -> int:
116
- """
117
- 把 argmax 的类别索引转成 int 标签。
118
- 若你的业务希望直接返回索引(如 0/1),这里就保持原样。
119
- """
120
- return int(idx)
121
-
122
-
123
- def _to_int_label_from_threshold(p_pos: float, threshold: float) -> int:
124
- """
125
- 使用阈值将正类概率转为 0/1 标签。
126
- """
127
- return int(p_pos >= threshold)
128
-
129
-
130
- # -----------------------------
131
- # Routes
132
- # -----------------------------
133
- @app.get("/")
134
- def root() -> Dict[str, Any]:
135
- return {
136
- "status": "ok",
137
- "model": MODEL_ID,
138
- "device": DEVICE,
139
- "max_length": MAX_LENGTH,
140
- "version": app.version,
141
- }
142
-
143
-
144
- @app.get("/healthz")
145
- def healthz() -> Dict[str, str]:
146
- return {"status": "ok"}
147
-
148
-
149
- @app.get("/readyz")
150
- def readyz() -> Dict[str, str]:
151
- try:
152
- _load_model()
153
- return {"status": "ready"}
154
- except Exception as e:
155
- return {"status": "error", "detail": str(e)}
156
-
157
-
158
- @app.post("/predict", response_model=PredictResponse)
159
- def predict(req: PredictRequest):
160
- """
161
- 批量预测入口:
162
- - 输入:texts: List[str]
163
- - 输出:每条文本的 {"label": int, "score": float},其中 score = 概率(类别1)
164
- - label 的生成规则:
165
- * 若提供 threshold:label = 1 当 概率(1) >= threshold;否则为 0
166
- * 不提供 threshold:label = argmax(prob)
167
- """
168
- if not req.texts:
169
- return {"results": []}
170
-
171
- try:
172
- _load_model()
173
-
174
- # Tokenize(批量),padding=True + truncation=True,长度可配
175
- encoded = _tokenizer(
176
- req.texts,
177
- padding=True,
178
- truncation=True,
179
- max_length=MAX_LENGTH,
180
- return_tensors="pt",
181
- ).to(DEVICE)
182
-
183
- with torch.no_grad():
184
- # autocast 仅在 FP16/GPU 开启时使用
185
- if DTYPE_FP16 and DEVICE == "cuda":
186
- with torch.cuda.amp.autocast(dtype=torch.float16):
187
- logits = _model(**encoded).logits
188
- else:
189
- logits = _model(**encoded).logits
190
-
191
- probs = torch.softmax(logits, dim=-1).detach().cpu().numpy()
192
- preds = probs.argmax(axis=-1)
193
-
194
- results: List[PredictResult] = []
195
- for i in range(len(req.texts)):
196
- # 二分类:取第1类(正类)的概率作为 score
197
- p_pos = float(probs[i, 1]) if probs.shape[1] > 1 else float(probs[i, 0])
198
-
199
- if req.threshold is not None and probs.shape[1] == 2:
200
- label = _to_int_label_from_threshold(p_pos, req.threshold)
201
- else:
202
- label = _to_int_label_from_argmax(int(preds[i]))
203
-
204
- results.append(PredictResult(label=label, score=p_pos))
205
-
206
- return {"results": [r.dict() for r in results]}
207
- except Exception as e:
208
- # 返回 500 + 错误信息(方便排障)
209
- return JSONResponse(status_code=500, content={"error": str(e)})
210
-
211
-
212
- # -----------------------------
213
- # Local dev entry
214
- # -----------------------------
215
- if __name__ == "__main__":
216
- import uvicorn
217
-
218
- uvicorn.run(
219
- "app:app",
220
- host=os.environ.get("HOST", "0.0.0.0"),
221
- port=int(os.environ.get("PORT", "7860")),
222
- reload=bool(int(os.environ.get("RELOAD", "0"))),
223
- )
 
1
+ from fastapi import FastAPI
2
+ from pydantic import BaseModel
3
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
4
+ import torch
5
+ import os
6
+
7
+
8
+ MODEL_ID = os.environ.get("MODEL_ID", "Perth0603/phishing-email-mobilebert")
9
+
10
+ app = FastAPI(title="Phishing Text Classifier", version="1.0.0")
11
+
12
+
13
+ class PredictPayload(BaseModel):
14
+ inputs: str
15
+
16
+
17
+ # Lazy singletons for model/tokenizer
18
+ _tokenizer = None
19
+ _model = None
20
+
21
+
22
+ def _load_model():
23
+ global _tokenizer, _model
24
+ if _tokenizer is None or _model is None:
25
+ _tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
26
+ _model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID)
27
+ # Warm-up
28
+ with torch.no_grad():
29
+ _ = _model(**_tokenizer(["warm up"], return_tensors="pt")).logits
30
+
31
+
32
+ @app.get("/")
33
+ def root():
34
+ return {"status": "ok", "model": MODEL_ID}
35
+
36
+
37
+ @app.post("/predict")
38
+ def predict(payload: PredictPayload):
39
+ _load_model()
40
+ try:
41
+ with torch.no_grad():
42
+ inputs = _tokenizer([payload.inputs], return_tensors="pt", truncation=True, max_length=512)
43
+ logits = _model(**inputs).logits
44
+ probs = torch.softmax(logits, dim=-1)[0]
45
+ score, idx = torch.max(probs, dim=0)
46
+ except Exception as e:
47
+ return {"error": str(e)}
48
+
49
+ # Map common ids to labels (kept generic; your config also has these)
50
+ id2label = {0: "LEGIT", 1: "PHISH"}
51
+ label = id2label.get(int(idx), str(int(idx)))
52
+ return {"label": label, "score": float(score)}
53
+
54
+