dongtruong1910
commited on
Commit
·
f817340
1
Parent(s):
ad5bfe2
Deploy
Browse files- Dockerfile +25 -0
- api.py +34 -0
- requirements.txt +6 -0
- saved_models/best_model.pth +3 -0
- src/__init__.py +0 -0
- src/configs.py +34 -0
- src/model.py +33 -0
- src/predict.py +119 -0
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 1. Chọn hệ điều hành Python 3.9
|
| 2 |
+
FROM python:3.9
|
| 3 |
+
|
| 4 |
+
# 2. Tạo thư mục làm việc
|
| 5 |
+
WORKDIR /code
|
| 6 |
+
|
| 7 |
+
# 3. Copy file requirements và cài đặt thư viện
|
| 8 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 9 |
+
# Cài torch bản CPU cho nhẹ (tùy chọn, hoặc cài thường cũng được)
|
| 10 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 11 |
+
|
| 12 |
+
# 4. Copy toàn bộ code vào trong
|
| 13 |
+
COPY ./src /code/src
|
| 14 |
+
COPY ./saved_models /code/saved_models
|
| 15 |
+
COPY ./api.py /code/api.py
|
| 16 |
+
|
| 17 |
+
# 5. Cấp quyền cho user (Hugging Face yêu cầu)
|
| 18 |
+
RUN useradd -m -u 1000 user
|
| 19 |
+
USER user
|
| 20 |
+
ENV HOME=/home/user \
|
| 21 |
+
PATH=/home/user/.local/bin:$PATH
|
| 22 |
+
|
| 23 |
+
# 6. Mở cổng 7860 (Cổng bắt buộc của Hugging Face)
|
| 24 |
+
# Lưu ý: Code api.py của bạn đang chạy port 8000, ta sẽ đổi lệnh chạy ở đây
|
| 25 |
+
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
|
api.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
import uvicorn
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 8 |
+
from src.predict import HateSpeechPredictor
|
| 9 |
+
|
| 10 |
+
app = FastAPI()
|
| 11 |
+
print("--> Đang khởi động Server...")
|
| 12 |
+
predictor = HateSpeechPredictor()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class Item(BaseModel):
|
| 16 |
+
text: str
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@app.post("/predict")
|
| 20 |
+
def predict(item: Item):
|
| 21 |
+
# Gọi hàm predict thông minh (đã xử lý đoạn văn)
|
| 22 |
+
result = predictor.predict(item.text)
|
| 23 |
+
|
| 24 |
+
return {
|
| 25 |
+
"text": item.text,
|
| 26 |
+
"prediction": result['label'],
|
| 27 |
+
"confidence": f"{result['confidence']:.2%}",
|
| 28 |
+
"is_toxic": result['is_toxic'],
|
| 29 |
+
"flagged_sentence": result['flagged_sentence']
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
transformers
|
| 3 |
+
pyvi
|
| 4 |
+
fastapi
|
| 5 |
+
uvicorn
|
| 6 |
+
pydantic
|
saved_models/best_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:09517912757395f534419b53f4f8cdaaa75d3ed6d16cc68da5d4c26cd43dc261
|
| 3 |
+
size 540084487
|
src/__init__.py
ADDED
|
File without changes
|
src/configs.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
# Lấy đường dẫn gốc của dự án
|
| 5 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Config:
|
| 9 |
+
# --- ĐƯỜNG DẪN DỮ LIỆU ---
|
| 10 |
+
TRAIN_PATH = os.path.join(BASE_DIR, 'data', 'raw', 'train.csv')
|
| 11 |
+
DEV_PATH = os.path.join(BASE_DIR, 'data', 'raw', 'dev.csv')
|
| 12 |
+
TEST_PATH = os.path.join(BASE_DIR, 'data', 'raw', 'test.csv')
|
| 13 |
+
|
| 14 |
+
# Nơi lưu model
|
| 15 |
+
MODEL_SAVE_PATH = os.path.join(BASE_DIR, 'saved_models', 'best_model.pth')
|
| 16 |
+
|
| 17 |
+
# --- CẤU HÌNH PHOBERT ---
|
| 18 |
+
MODEL_NAME = "vinai/phobert-base"
|
| 19 |
+
|
| 20 |
+
# Tham số xử lý văn bản
|
| 21 |
+
MAX_LEN = 100 # Độ dài câu tối đa
|
| 22 |
+
N_CLASSES = 3 # <--- DÒNG BẠN ĐANG THIẾU (0: Clean, 1: Offensive, 2: Hate)
|
| 23 |
+
|
| 24 |
+
# --- THAM SỐ HUẤN LUYỆN (Fine-tuning) ---
|
| 25 |
+
BATCH_SIZE = 16 # PhoBERT nặng nên để batch size nhỏ (16 hoặc 8)
|
| 26 |
+
EPOCHS = 10
|
| 27 |
+
LEARNING_RATE = 2e-5 # Learning rate rất nhỏ cho Transformer
|
| 28 |
+
|
| 29 |
+
# Tự động chọn GPU
|
| 30 |
+
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
if __name__ == '__main__':
|
| 34 |
+
print(f"Device: {Config.DEVICE}")
|
src/model.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
from transformers import AutoModel
|
| 3 |
+
from .configs import Config
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class HateSpeechModel(nn.Module):
|
| 7 |
+
def __init__(self, n_classes):
|
| 8 |
+
super(HateSpeechModel, self).__init__()
|
| 9 |
+
# Load khung xương PhoBERT
|
| 10 |
+
self.bert = AutoModel.from_pretrained(Config.MODEL_NAME, weights_only=False)
|
| 11 |
+
|
| 12 |
+
# Khóa bớt các tầng đầu để train nhanh hơn (Optional - Tùy chọn)
|
| 13 |
+
for param in self.bert.parameters():
|
| 14 |
+
param.requires_grad = True
|
| 15 |
+
|
| 16 |
+
# Thêm đầu ra phân loại
|
| 17 |
+
self.drop = nn.Dropout(p=0.3)
|
| 18 |
+
self.fc = nn.Linear(768, n_classes) # 768 là kích thước vector của PhoBERT Base
|
| 19 |
+
|
| 20 |
+
def forward(self, input_ids, attention_mask):
|
| 21 |
+
# Cho dữ liệu chạy qua PhoBERT
|
| 22 |
+
# output[0] là hidden states, output[1] là pooled output (vector đại diện câu)
|
| 23 |
+
outputs = self.bert(
|
| 24 |
+
input_ids=input_ids,
|
| 25 |
+
attention_mask=attention_mask
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Lấy vector đại diện của token [CLS] (token đầu tiên)
|
| 29 |
+
# Nó chứa ý nghĩa của toàn bộ câu
|
| 30 |
+
pooled_output = outputs[1]
|
| 31 |
+
|
| 32 |
+
output = self.drop(pooled_output)
|
| 33 |
+
return self.fc(output)
|
src/predict.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
from transformers import AutoTokenizer
|
| 4 |
+
from pyvi import ViTokenizer
|
| 5 |
+
from src.configs import Config
|
| 6 |
+
from src.model import HateSpeechModel
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class HateSpeechPredictor:
|
| 12 |
+
def __init__(self, model_path=None):
|
| 13 |
+
self.device = Config.DEVICE
|
| 14 |
+
print(f"--> Đang khởi tạo Predictor trên: {self.device}")
|
| 15 |
+
|
| 16 |
+
# 1. Load Tokenizer & Model
|
| 17 |
+
self.tokenizer = AutoTokenizer.from_pretrained(Config.MODEL_NAME)
|
| 18 |
+
self.model = HateSpeechModel(n_classes=Config.N_CLASSES)
|
| 19 |
+
|
| 20 |
+
# 2. Load Weights
|
| 21 |
+
if model_path is None:
|
| 22 |
+
model_path = Config.MODEL_SAVE_PATH
|
| 23 |
+
|
| 24 |
+
if os.path.exists(model_path):
|
| 25 |
+
self.model.load_state_dict(torch.load(model_path, map_location=self.device))
|
| 26 |
+
self.model.to(self.device)
|
| 27 |
+
self.model.eval()
|
| 28 |
+
print("--> Đã load model thành công!")
|
| 29 |
+
else:
|
| 30 |
+
raise FileNotFoundError(f"Chưa có file model tại {model_path}")
|
| 31 |
+
|
| 32 |
+
# Map nhãn
|
| 33 |
+
self.labels_map = {0: "CLEAN", 1: "OFFENSIVE", 2: "HATE"}
|
| 34 |
+
# Map mức độ nghiêm trọng (để so sánh)
|
| 35 |
+
self.severity_map = {"CLEAN": 0, "OFFENSIVE": 1, "HATE": 2}
|
| 36 |
+
|
| 37 |
+
def _split_sentences(self, text):
|
| 38 |
+
"""Hàm tách đoạn văn thành các câu nhỏ"""
|
| 39 |
+
# Tách dựa trên dấu chấm, chấm than, chấm hỏi, hoặc xuống dòng
|
| 40 |
+
sentences = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?|\!|\n)\s', text)
|
| 41 |
+
return [s.strip() for s in sentences if len(s.strip()) > 1]
|
| 42 |
+
|
| 43 |
+
def _predict_single(self, text):
|
| 44 |
+
"""Dự đoán cho 1 câu đơn"""
|
| 45 |
+
text_segmented = ViTokenizer.tokenize(text)
|
| 46 |
+
|
| 47 |
+
encoding = self.tokenizer.encode_plus(
|
| 48 |
+
text_segmented,
|
| 49 |
+
max_length=Config.MAX_LEN,
|
| 50 |
+
truncation=True,
|
| 51 |
+
padding='max_length',
|
| 52 |
+
add_special_tokens=True,
|
| 53 |
+
return_attention_mask=True,
|
| 54 |
+
return_tensors='pt'
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
input_ids = encoding['input_ids'].to(self.device)
|
| 58 |
+
attention_mask = encoding['attention_mask'].to(self.device)
|
| 59 |
+
|
| 60 |
+
with torch.no_grad():
|
| 61 |
+
outputs = self.model(input_ids, attention_mask)
|
| 62 |
+
probs = F.softmax(outputs, dim=1)
|
| 63 |
+
|
| 64 |
+
max_prob, pred_idx = torch.max(probs, dim=1)
|
| 65 |
+
return self.labels_map[pred_idx.item()], max_prob.item()
|
| 66 |
+
|
| 67 |
+
def predict(self, text):
|
| 68 |
+
"""
|
| 69 |
+
Hàm chính: Xử lý cả đoạn văn.
|
| 70 |
+
Logic: Tách câu -> Dự đoán từng câu -> Lấy nhãn NẶNG NHẤT.
|
| 71 |
+
"""
|
| 72 |
+
sentences = self._split_sentences(text)
|
| 73 |
+
|
| 74 |
+
final_label = "CLEAN"
|
| 75 |
+
final_conf = 0.0
|
| 76 |
+
max_severity = 0
|
| 77 |
+
flagged_sentence = "" # Lưu lại câu bị vi phạm
|
| 78 |
+
|
| 79 |
+
# Nếu đoạn văn quá ngắn hoặc không tách được, coi là 1 câu
|
| 80 |
+
if len(sentences) == 0:
|
| 81 |
+
sentences = [text]
|
| 82 |
+
|
| 83 |
+
for sent in sentences:
|
| 84 |
+
label, conf = self._predict_single(sent)
|
| 85 |
+
severity = self.severity_map[label]
|
| 86 |
+
|
| 87 |
+
# Cập nhật nếu tìm thấy câu nặng hơn (HATE > OFFENSIVE > CLEAN)
|
| 88 |
+
# Hoặc cùng mức độ nhưng độ tin cậy cao hơn
|
| 89 |
+
if severity > max_severity:
|
| 90 |
+
max_severity = severity
|
| 91 |
+
final_label = label
|
| 92 |
+
final_conf = conf
|
| 93 |
+
flagged_sentence = sent
|
| 94 |
+
elif severity == max_severity and conf > final_conf:
|
| 95 |
+
final_conf = conf
|
| 96 |
+
flagged_sentence = sent
|
| 97 |
+
|
| 98 |
+
# Nếu là CLEAN thì không cần flagged_sentence
|
| 99 |
+
if final_label == "CLEAN":
|
| 100 |
+
flagged_sentence = None
|
| 101 |
+
|
| 102 |
+
return {
|
| 103 |
+
"label": final_label,
|
| 104 |
+
"confidence": final_conf,
|
| 105 |
+
"is_toxic": final_label != "CLEAN",
|
| 106 |
+
"flagged_sentence": flagged_sentence # Câu "tội đồ" làm bài bị chặn
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# Test
|
| 111 |
+
if __name__ == "__main__":
|
| 112 |
+
p = HateSpeechPredictor()
|
| 113 |
+
# Test đoạn văn dài
|
| 114 |
+
paragraph = "Hôm nay trời đẹp. Nhưng mày là đồ ngu. Đi chơi thôi."
|
| 115 |
+
result = p.predict(paragraph)
|
| 116 |
+
|
| 117 |
+
print(f"Input: {paragraph}")
|
| 118 |
+
print(f"Kết quả: {result['label']} ({result['confidence']:.2%})")
|
| 119 |
+
print(f"Câu vi phạm: {result['flagged_sentence']}")
|