youssefreda9 commited on
Commit
f8bb786
·
1 Parent(s): e9527e6

fix(autocomplete): GPT-2 now generates complete words, not BPE subword tokens

Browse files

BEFORE: 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.6 # Weight: 60% bigram, 40% GPT-2
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 thinks fit the context but bigram doesn't know
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
- # Give these a lower alpha since they have no bigram backing
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
- """Get GPT-2 next-token probability distribution."""
 
 
 
 
 
 
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, # Shorter than 1024 for speed
301
  )
 
302
 
 
303
  with torch.no_grad():
304
- outputs = self.gpt2_model(**inputs)
305
- logits = outputs.logits[0, -1]
306
-
307
- probs = torch.softmax(logits, dim=-1)
308
- top_probs, top_ids = torch.topk(probs, top_k)
309
-
 
 
 
 
 
 
 
 
310
  prob_dict = {}
311
- for idx, prob in zip(top_ids, top_probs):
312
- word = self.gpt2_tokenizer.decode([idx]).strip()
313
- if word and len(word) >= 2:
314
- prob_dict[word] = prob.item()
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: