toxicity / app.py
Võ Minh Trí
.
2104720
Raw
History Blame Contribute Delete
4.12 kB
import time
import joblib
import numpy as np
import torch
from fastapi import FastAPI
from pydantic import BaseModel
from transformers import AutoTokenizer, AutoModelForSequenceClassification, RobertaConfig, RobertaForSequenceClassification
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
SENTIMENT_MODEL_ID = "vanhai123/phobert-vi-comment-4class"
HATE_MODEL_ID = "visolex/visobert-hsd"
MAX_LENGTH = 128
torch.set_num_threads(2) # Tối ưu hóa khớp với cấu hình 2 vCPU basic free của HF Space
app = FastAPI(title="Vietnamese Hate Speech Detection API")
print("Loading models... (chỉ chạy 1 lần khi Space khởi động)")
# 1. Load Sentiment Model
sent_tok = AutoTokenizer.from_pretrained(SENTIMENT_MODEL_ID)
sent_model = AutoModelForSequenceClassification.from_pretrained(SENTIMENT_MODEL_ID).eval()
sent_labels = [sent_model.config.id2label[i] for i in range(sent_model.config.num_labels)]
# 2. Load Hate Model (Áp dụng bản vá map cấu hình và trọng số thủ công)
hate_tok = AutoTokenizer.from_pretrained(HATE_MODEL_ID)
hate_config = RobertaConfig(
vocab_size=15004,
hidden_size=768,
num_hidden_layers=12,
num_attention_heads=12,
intermediate_size=3072,
max_position_embeddings=514,
type_vocab_size=2,
pad_token_id=1,
bos_token_id=0,
eos_token_id=2,
num_labels=3,
id2label={0: "CLEAN", 1: "OFFENSIVE", 2: "HATE"},
label2id={"CLEAN": 0, "OFFENSIVE": 1, "HATE": 2}
)
hate_model = RobertaForSequenceClassification(hate_config)
# Tải file trọng số gốc về Space ảo
weights_path = hf_hub_download(repo_id=HATE_MODEL_ID, filename="model.safetensors")
state_dict = load_file(weights_path)
# Đổi lại tiền tố biến từ encoder sang roberta
fixed_state_dict = {}
for key, value in state_dict.items():
new_key = key
if key.startswith("encoder."):
new_key = key.replace("encoder.", "roberta.", 1)
fixed_state_dict[new_key] = value
hate_model.load_state_dict(fixed_state_dict, strict=False)
hate_model.eval()
hate_labels = [hate_model.config.id2label[i] for i in range(hate_model.config.num_labels)]
# 3. Load Meta-Classifier từ file joblib (1.28 KB)
meta_bundle = joblib.load("meta_classifier.joblib")
meta_clf = meta_bundle["model"]
print("Models loaded successfully. Ready to serve.")
@torch.no_grad()
def get_probs(text, tok, model):
inputs = tok(text, truncation=True, max_length=MAX_LENGTH, return_tensors="pt")
logits = model(**inputs).logits
probs = torch.softmax(logits, dim=-1).squeeze(0).numpy()
return probs
class PredictRequest(BaseModel):
text: str
class PredictResponse(BaseModel):
final_label: str
confidence: float
sentiment: dict
hate_speech: dict
latency_ms: float
@app.get("/")
def health():
return {"status": "ok"}
@app.post("/predict", response_model=PredictResponse)
def predict(req: PredictRequest):
start = time.time()
# Trích xuất phân phối xác suất từ 2 model lõi
p_sent = get_probs(req.text, sent_tok, sent_model)
p_hate = get_probs(req.text, hate_tok, hate_model)
# Lấy label dự đoán của riêng từng model độc lập
sent_label = sent_labels[int(np.argmax(p_sent))]
hate_label = hate_labels[int(np.argmax(p_hate))]
# Ghép chuỗi xác suất thành vector đặc trưng (7 chiều) truyền vào Meta Classifier
features = np.concatenate([p_sent, p_hate]).reshape(1, -1)
final_pred = meta_clf.predict(features)[0]
final_proba = meta_clf.predict_proba(features)[0]
confidence = float(np.max(final_proba))
elapsed_ms = (time.time() - start) * 1000
return {
"final_label": final_pred,
"confidence": round(confidence, 4),
"sentiment": {
"label": sent_label,
"scores": {l: round(float(s), 4) for l, s in zip(sent_labels, p_sent)},
},
"hate_speech": {
"label": hate_label,
"scores": {l: round(float(s), 4) for l, s in zip(hate_labels, p_hate)},
},
"latency_ms": round(elapsed_ms, 1),
}