""" Model Loader - Tải model và tokenizer từ HuggingFace Hub. Module này chịu trách nhiệm: 1. Tải model + tokenizer DUY NHẤT MỘT LẦN khi app khởi động 2. Chuyển model sang eval() mode để tối ưu inference 3. Cung cấp hàm generate_embeddings() để trích xuất vector nhúng """ import torch from transformers import AutoModel, AutoTokenizer from app.config import settings # Biến toàn cục - lưu trữ model và tokenizer sau khi load model = None tokenizer = None def load_model(): """ Tải model và tokenizer từ HuggingFace Hub vào bộ nhớ. Hàm này được gọi DUY NHẤT MỘT LẦN khi FastAPI app khởi động. Model được chuyển sang eval() mode và đưa lên device phù hợp (CPU/GPU). """ global model, tokenizer print(f"[INFO] Loading model: {settings.MODEL_NAME}") print(f"[INFO] Device: {settings.DEVICE}") # Load tokenizer tokenizer = AutoTokenizer.from_pretrained( settings.MODEL_NAME, token=settings.HF_TOKEN, ) # Load model model = AutoModel.from_pretrained( settings.MODEL_NAME, token=settings.HF_TOKEN, ) # Chuyển sang eval mode (tắt dropout, batch norm ở inference mode) model.eval() # Đưa model lên device (cpu hoặc cuda) model.to(settings.DEVICE) print("[INFO] Model loaded successfully! Ready to serve.") def generate_embeddings(texts: list[str]) -> list[list[float]]: """ Trích xuất vector nhúng từ danh sách văn bản. Pipeline xử lý: 1. Tokenize input texts 2. Feed tokens qua model (không tính gradient) 3. Mean pooling trên token embeddings (bỏ qua padding tokens) 4. L2 normalize vector kết quả Args: texts: Danh sách các chuỗi văn bản cần embedding. Returns: Danh sách các vector nhúng (mỗi vector là list[float]). """ # Bước 1: Tokenize encoded = tokenizer( texts, padding=True, truncation=True, max_length=settings.MAX_LENGTH, return_tensors="pt", ) # Đưa input tensors lên cùng device với model encoded = {key: val.to(settings.DEVICE) for key, val in encoded.items()} # Bước 2: Forward pass (không tính gradient để tiết kiệm bộ nhớ) with torch.no_grad(): outputs = model(**encoded) # Bước 3: Mean Pooling # outputs.last_hidden_state có shape: (batch_size, seq_length, hidden_dim) token_embeddings = outputs.last_hidden_state attention_mask = encoded["attention_mask"] # Mở rộng attention_mask để khớp shape với token_embeddings # attention_mask: (batch_size, seq_length) -> (batch_size, seq_length, hidden_dim) mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() # Tính trung bình có trọng số (chỉ tính các token thực, bỏ qua padding) sum_embeddings = torch.sum(token_embeddings * mask_expanded, dim=1) sum_mask = torch.clamp(mask_expanded.sum(dim=1), min=1e-9) mean_embeddings = sum_embeddings / sum_mask # Bước 4: L2 Normalize normalized = torch.nn.functional.normalize(mean_embeddings, p=2, dim=1) # Chuyển tensor về list Python return normalized.cpu().tolist() def compute_cosine_similarity(vector1: list[float], vector2: list[float]) -> float: """ Tinh cosine similarity giua 2 vector embedding. Cosine similarity do goc giua 2 vector trong khong gian nhieu chieu. Gia tri tra ve nam trong khoang [-1, 1]: - 1.0 = hoan toan giong nhau - 0.0 = khong lien quan - -1.0 = hoan toan doi lap Args: vector1: Vector embedding thu nhat (list[float]). vector2: Vector embedding thu hai (list[float]). Returns: Diem cosine similarity (float). """ # Chuyen list Python thanh tensor v1 = torch.tensor(vector1, dtype=torch.float32) v2 = torch.tensor(vector2, dtype=torch.float32) # Tinh cosine similarity bang PyTorch similarity = torch.nn.functional.cosine_similarity( v1.unsqueeze(0), v2.unsqueeze(0) ) return similarity.item() # ==================== MINILM MODEL ==================== # Bien toan cuc MiniLM - tach biet hoan toan voi Gemma minilm_model = None minilm_tokenizer = None def load_minilm(): """ Tai MiniLM model va tokenizer tu HuggingFace Hub. MiniLM chay tren CPU vi: - Model chi co 22M params, CPU van cuc nhanh (~5-10ms/request) - Tiet kiem GPU memory cho Gemma 300M (model chinh) """ global minilm_model, minilm_tokenizer print(f"[INFO] Loading MiniLM: {settings.MODEL_NAME_MINILM}") print("[INFO] MiniLM device: cpu") # Load tokenizer minilm_tokenizer = AutoTokenizer.from_pretrained( settings.MODEL_NAME_MINILM, ) # Load model minilm_model = AutoModel.from_pretrained( settings.MODEL_NAME_MINILM, ) # Eval mode + CPU minilm_model.eval() minilm_model.to("cpu") print("[INFO] MiniLM loaded successfully!") def generate_embeddings_minilm(texts: list[str]) -> list[list[float]]: """ Trich xuat vector nhung bang MiniLM (384 chieu). Pipeline giong voi Gemma nhung: - Chay tren CPU (khong dung GPU) - Vector output co 384 chieu (thay vi 768 cua Gemma) - MAX_LENGTH ngan hon (256 thay vi 512) Args: texts: Danh sach van ban can embedding (search query, job title, ...). Returns: Danh sach vector nhung 384 chieu. """ # Buoc 1: Tokenize encoded = minilm_tokenizer( texts, padding=True, truncation=True, max_length=settings.MAX_LENGTH_MINILM, return_tensors="pt", ) # MiniLM chay tren CPU encoded = {key: val.to("cpu") for key, val in encoded.items()} # Buoc 2: Forward pass with torch.no_grad(): outputs = minilm_model(**encoded) # Buoc 3: Mean Pooling token_embeddings = outputs.last_hidden_state attention_mask = encoded["attention_mask"] mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() sum_embeddings = torch.sum(token_embeddings * mask_expanded, dim=1) sum_mask = torch.clamp(mask_expanded.sum(dim=1), min=1e-9) mean_embeddings = sum_embeddings / sum_mask # Buoc 4: L2 Normalize normalized = torch.nn.functional.normalize(mean_embeddings, p=2, dim=1) return normalized.cpu().tolist()