Commit ·
f8bb786
1
Parent(s): e9527e6
fix(autocomplete): GPT-2 now generates complete words, not BPE subword tokens
Browse filesBEFORE: GPT-2 predicted next BPE token → 'ال' instead of 'القاهرة'
AFTER: GPT-2 uses generate() with beam search → complete Arabic words
Changes:
- model.generate(max_new_tokens=6, num_beams=10, num_return_sequences=10)
- Extract first complete Arabic word from each generated sequence
- alpha changed from 0.6 bigram / 0.4 gpt2 → 0.4 / 0.6 (GPT-2 has context!)
- GPT-2 bonus candidates weight increased from 0.3 to 0.5
- Added [GPT2] log line showing generated words for debugging
src/nlp/autocomplete/autocomplete_service.py
CHANGED
|
@@ -74,7 +74,7 @@ class HybridAutoComplete:
|
|
| 74 |
self.gpt2_tokenizer = None
|
| 75 |
self.gpt2_model = None
|
| 76 |
self.device = "cpu"
|
| 77 |
-
self.alpha = 0.
|
| 78 |
self._cache = {}
|
| 79 |
self._cache_max = 256
|
| 80 |
|
|
@@ -274,11 +274,10 @@ class HybridAutoComplete:
|
|
| 274 |
seen_words.add(w)
|
| 275 |
|
| 276 |
# 5. ADD GPT-2's own top contextual predictions as bonus candidates
|
| 277 |
-
# These are words GPT-2
|
| 278 |
for w, neural_p in sorted(gpt2_probs.items(), key=lambda x: x[1], reverse=True)[:10]:
|
| 279 |
if w not in seen_words and len(w) >= 2:
|
| 280 |
-
|
| 281 |
-
score = 0.3 * neural_p # Lower weight, but still contextual
|
| 282 |
results.append((w, score))
|
| 283 |
seen_words.add(w)
|
| 284 |
|
|
@@ -288,7 +287,13 @@ class HybridAutoComplete:
|
|
| 288 |
return [w for w, _ in results[:n]]
|
| 289 |
|
| 290 |
def _gpt2_next_token_probs(self, prefix: str, top_k: int = 50) -> dict:
|
| 291 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
if self.gpt2_model is None or self.gpt2_tokenizer is None:
|
| 293 |
return {}
|
| 294 |
|
|
@@ -297,22 +302,50 @@ class HybridAutoComplete:
|
|
| 297 |
prefix,
|
| 298 |
return_tensors="pt",
|
| 299 |
truncation=True,
|
| 300 |
-
max_length=512,
|
| 301 |
)
|
|
|
|
| 302 |
|
|
|
|
| 303 |
with torch.no_grad():
|
| 304 |
-
outputs = self.gpt2_model(
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
prob_dict = {}
|
| 311 |
-
for
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
return prob_dict
|
| 317 |
|
| 318 |
except Exception as e:
|
|
|
|
| 74 |
self.gpt2_tokenizer = None
|
| 75 |
self.gpt2_model = None
|
| 76 |
self.device = "cpu"
|
| 77 |
+
self.alpha = 0.4 # Weight: 40% bigram, 60% GPT-2 (GPT-2 has context!)
|
| 78 |
self._cache = {}
|
| 79 |
self._cache_max = 256
|
| 80 |
|
|
|
|
| 274 |
seen_words.add(w)
|
| 275 |
|
| 276 |
# 5. ADD GPT-2's own top contextual predictions as bonus candidates
|
| 277 |
+
# These are complete words GPT-2 generated based on full sentence context
|
| 278 |
for w, neural_p in sorted(gpt2_probs.items(), key=lambda x: x[1], reverse=True)[:10]:
|
| 279 |
if w not in seen_words and len(w) >= 2:
|
| 280 |
+
score = 0.5 * neural_p # High weight — GPT-2 understands context
|
|
|
|
| 281 |
results.append((w, score))
|
| 282 |
seen_words.add(w)
|
| 283 |
|
|
|
|
| 287 |
return [w for w, _ in results[:n]]
|
| 288 |
|
| 289 |
def _gpt2_next_token_probs(self, prefix: str, top_k: int = 50) -> dict:
|
| 290 |
+
"""
|
| 291 |
+
Get GPT-2 next-WORD predictions using generate() for complete words.
|
| 292 |
+
|
| 293 |
+
Instead of predicting a single BPE token (which gives subwords like 'ال'),
|
| 294 |
+
we generate short sequences and extract the first complete word.
|
| 295 |
+
This gives us actual Arabic words like 'القاهرة' instead of 'ال'.
|
| 296 |
+
"""
|
| 297 |
if self.gpt2_model is None or self.gpt2_tokenizer is None:
|
| 298 |
return {}
|
| 299 |
|
|
|
|
| 302 |
prefix,
|
| 303 |
return_tensors="pt",
|
| 304 |
truncation=True,
|
| 305 |
+
max_length=512,
|
| 306 |
)
|
| 307 |
+
input_len = inputs['input_ids'].shape[1]
|
| 308 |
|
| 309 |
+
# Generate multiple sequences (beam search for diversity)
|
| 310 |
with torch.no_grad():
|
| 311 |
+
outputs = self.gpt2_model.generate(
|
| 312 |
+
**inputs,
|
| 313 |
+
max_new_tokens=6, # Enough tokens to form a complete word
|
| 314 |
+
num_beams=10, # Beam search for quality
|
| 315 |
+
num_return_sequences=10,
|
| 316 |
+
do_sample=False,
|
| 317 |
+
early_stopping=True,
|
| 318 |
+
no_repeat_ngram_size=2,
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
# Extract the first NEW word from each generated sequence
|
| 322 |
+
import re
|
| 323 |
+
ARABIC_WORD_RE = re.compile(r'[\u0600-\u06FF]{2,}')
|
| 324 |
+
|
| 325 |
prob_dict = {}
|
| 326 |
+
for i, seq in enumerate(outputs):
|
| 327 |
+
# Decode only the NEW tokens (skip the input prefix)
|
| 328 |
+
new_tokens = seq[input_len:]
|
| 329 |
+
generated_text = self.gpt2_tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
|
| 330 |
+
|
| 331 |
+
if not generated_text:
|
| 332 |
+
continue
|
| 333 |
+
|
| 334 |
+
# Extract the first complete Arabic word
|
| 335 |
+
match = ARABIC_WORD_RE.search(generated_text)
|
| 336 |
+
if match:
|
| 337 |
+
word = match.group(0)
|
| 338 |
+
# Score: higher rank = higher probability (beam search is sorted)
|
| 339 |
+
score = 1.0 / (i + 1) # 1.0, 0.5, 0.33, 0.25, ...
|
| 340 |
+
if word not in prob_dict or prob_dict[word] < score:
|
| 341 |
+
prob_dict[word] = score
|
| 342 |
+
|
| 343 |
+
# Normalize scores to sum to ~1
|
| 344 |
+
total = sum(prob_dict.values())
|
| 345 |
+
if total > 0:
|
| 346 |
+
prob_dict = {w: s / total for w, s in prob_dict.items()}
|
| 347 |
+
|
| 348 |
+
logger.info(f"[GPT2] context='{prefix[-40:]}' → words={list(prob_dict.keys())[:8]}")
|
| 349 |
return prob_dict
|
| 350 |
|
| 351 |
except Exception as e:
|