File size: 6,484 Bytes
7b60154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
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()