youssefreda9 commited on
Commit
ea6d242
·
1 Parent(s): 964adc9

fix: Switch grammar to greedy decoding + stronger anti-hallucination - num_beams=1 (greedy, most stable for seq2seq correction) - repetition_penalty=2.0 (strong anti-repeat) - Log raw model output before hallucination check - Tightened bigram duplicate detection

Browse files
Files changed (1) hide show
  1. src/nlp/grammar/grammar_service.py +33 -42
src/nlp/grammar/grammar_service.py CHANGED
@@ -17,7 +17,6 @@ MODEL_ID = "bayan10/Bayan_Arabic_Grammar"
17
 
18
  # Generation parameters
19
  MAX_INPUT_LENGTH = 512
20
- NUM_BEAMS = 2 # Lower beams = less hallucination on CPU
21
 
22
 
23
  def get_grammar_model():
@@ -51,7 +50,6 @@ def get_grammar_model():
51
 
52
  except Exception as e:
53
  logger.error(f"Failed to load grammar model: {e}")
54
- # Try without local_files_only as fallback
55
  try:
56
  logger.info("Retrying without local_files_only...")
57
  _grammar_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
@@ -68,52 +66,44 @@ def get_grammar_model():
68
  return _grammar_model, _grammar_tokenizer, _device
69
 
70
 
71
- def _word_set(text: str) -> set:
72
- """Extract set of Arabic words from text."""
73
- return set(re.findall(r'[\u0600-\u06FF]+', text))
74
-
75
-
76
  def _is_hallucinated(original: str, corrected: str) -> bool:
77
  """
78
  Check if the grammar output is hallucinated / garbled.
79
  Returns True if output should be discarded.
80
  """
81
- # 1. Output too long (grammar should NOT add lots of text)
 
 
 
82
  if len(corrected) > len(original) * 1.5:
83
- logger.warning(f"Grammar hallucination: output too long ({len(corrected)} vs {len(original)})")
84
  return True
85
 
86
  # 2. Output too short
87
  if len(corrected) < len(original) * 0.5:
88
- logger.warning(f"Grammar hallucination: output too short ({len(corrected)} vs {len(original)})")
89
  return True
90
 
91
- # 3. Check word order preservation — grammar should NOT rearrange sentences
92
- orig_words = re.findall(r'[\u0600-\u06FF]+', original)
93
- corr_words = re.findall(r'[\u0600-\u06FF]+', corrected)
94
-
95
- if len(corr_words) > len(orig_words) * 1.5:
96
- logger.warning(f"Grammar hallucination: too many words ({len(corr_words)} vs {len(orig_words)})")
97
  return True
98
 
99
- # 4. Check for duplicated segments (repeated phrases)
100
- if len(corr_words) > 3:
101
- # Check if any 3-word sequence appears more than once
102
- trigrams = [' '.join(corr_words[i:i+3]) for i in range(len(corr_words)-2)]
103
- if len(trigrams) != len(set(trigrams)):
104
- logger.warning("Grammar hallucination: repeated phrases detected")
105
  return True
106
 
107
- # 5. First word of output should be close to first word of input
108
- # (grammar shouldn't rearrange sentence start)
109
  if orig_words and corr_words:
110
- if orig_words[0] != corr_words[0]:
111
- # Check if the first few words are completely rearranged
112
- orig_start = set(orig_words[:3])
113
- corr_start = set(corr_words[:3])
114
- if not orig_start.intersection(corr_start):
115
- logger.warning("Grammar hallucination: sentence completely rearranged")
116
- return True
117
 
118
  return False
119
 
@@ -139,30 +129,31 @@ def correct_grammar(text: str) -> str:
139
  )
140
  inputs = {k: v.to(device) for k, v in inputs.items()}
141
 
142
- # Calculate dynamic max_length: slightly more than input to allow small expansions
143
  input_len = inputs['input_ids'].shape[1]
144
- dynamic_max_len = min(int(input_len * 1.3) + 10, MAX_INPUT_LENGTH)
145
 
146
- # Generate correction with conservative params to avoid hallucination
147
  with torch.no_grad():
148
  outputs = model.generate(
149
  **inputs,
150
  max_length=dynamic_max_len,
151
- num_beams=NUM_BEAMS,
152
- early_stopping=True,
153
- no_repeat_ngram_size=3,
154
- length_penalty=1.0,
155
- repetition_penalty=1.5,
156
  )
157
 
158
  # Decode output
159
- corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
 
 
160
 
161
  # Safety: if model returns empty, keep original
162
- if not corrected or not corrected.strip():
 
163
  return text
164
 
165
- corrected = corrected.strip()
166
 
167
  # Check for hallucination
168
  if _is_hallucinated(text, corrected):
 
17
 
18
  # Generation parameters
19
  MAX_INPUT_LENGTH = 512
 
20
 
21
 
22
  def get_grammar_model():
 
50
 
51
  except Exception as e:
52
  logger.error(f"Failed to load grammar model: {e}")
 
53
  try:
54
  logger.info("Retrying without local_files_only...")
55
  _grammar_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
 
66
  return _grammar_model, _grammar_tokenizer, _device
67
 
68
 
 
 
 
 
 
69
  def _is_hallucinated(original: str, corrected: str) -> bool:
70
  """
71
  Check if the grammar output is hallucinated / garbled.
72
  Returns True if output should be discarded.
73
  """
74
+ orig_words = re.findall(r'[\u0600-\u06FF]+', original)
75
+ corr_words = re.findall(r'[\u0600-\u06FF]+', corrected)
76
+
77
+ # 1. Output too long (grammar corrections are minor edits)
78
  if len(corrected) > len(original) * 1.5:
79
+ logger.warning(f"Hallucination: output too long ({len(corrected)} vs {len(original)} chars)")
80
  return True
81
 
82
  # 2. Output too short
83
  if len(corrected) < len(original) * 0.5:
84
+ logger.warning(f"Hallucination: output too short ({len(corrected)} vs {len(original)} chars)")
85
  return True
86
 
87
+ # 3. Too many words added
88
+ if len(corr_words) > len(orig_words) * 1.4:
89
+ logger.warning(f"Hallucination: too many words ({len(corr_words)} vs {len(orig_words)})")
 
 
 
90
  return True
91
 
92
+ # 4. Check for duplicated 2-word phrases (bigrams)
93
+ if len(corr_words) > 4:
94
+ bigrams = [f"{corr_words[i]} {corr_words[i+1]}" for i in range(len(corr_words)-1)]
95
+ if len(bigrams) != len(set(bigrams)):
96
+ logger.warning("Hallucination: repeated bigrams detected")
 
97
  return True
98
 
99
+ # 5. Sentence start rearranged (grammar shouldn't move first word to end)
 
100
  if orig_words and corr_words:
101
+ # Check if first 2 original words appear in first 4 corrected words
102
+ orig_head = set(orig_words[:2])
103
+ corr_head = set(corr_words[:4])
104
+ if len(orig_head) > 0 and not orig_head.intersection(corr_head):
105
+ logger.warning("Hallucination: sentence start completely rearranged")
106
+ return True
 
107
 
108
  return False
109
 
 
129
  )
130
  inputs = {k: v.to(device) for k, v in inputs.items()}
131
 
132
+ # Dynamic max_length: close to input to prevent hallucination
133
  input_len = inputs['input_ids'].shape[1]
134
+ dynamic_max_len = min(int(input_len * 1.3) + 5, MAX_INPUT_LENGTH)
135
 
136
+ # Strategy: greedy decoding (most stable for grammar correction)
137
  with torch.no_grad():
138
  outputs = model.generate(
139
  **inputs,
140
  max_length=dynamic_max_len,
141
+ num_beams=1, # Greedy decoding — most stable
142
+ do_sample=False,
143
+ repetition_penalty=2.0, # Strong penalty against repeating
 
 
144
  )
145
 
146
  # Decode output
147
+ raw_output = tokenizer.decode(outputs[0], skip_special_tokens=True)
148
+ logger.info(f"[GRAMMAR-RAW] input({len(text)}): '{text}'")
149
+ logger.info(f"[GRAMMAR-RAW] output({len(raw_output)}): '{raw_output}'")
150
 
151
  # Safety: if model returns empty, keep original
152
+ if not raw_output or not raw_output.strip():
153
+ logger.warning("Grammar returned empty output")
154
  return text
155
 
156
+ corrected = raw_output.strip()
157
 
158
  # Check for hallucination
159
  if _is_hallucinated(text, corrected):