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

fix: Prevent grammar model hallucination (rearranging/duplicating text) - Dynamic max_length (1.3x input instead of fixed 512) - repetition_penalty=1.5 to discourage repeating - Reduce beams 4→2 (less hallucination on CPU) - Add _is_hallucinated() safety checker: output length, word count, repeated phrases, sentence rearrangement

Browse files
Files changed (1) hide show
  1. src/nlp/grammar/grammar_service.py +66 -9
src/nlp/grammar/grammar_service.py CHANGED
@@ -3,6 +3,7 @@ Grammar correction service using Bayan_Arabic_Grammar (T5).
3
  Lazy-loads the model on first request, keeps it resident in memory.
4
  """
5
  import logging
 
6
  import torch
7
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
8
 
@@ -16,8 +17,7 @@ MODEL_ID = "bayan10/Bayan_Arabic_Grammar"
16
 
17
  # Generation parameters
18
  MAX_INPUT_LENGTH = 512
19
- MAX_OUTPUT_LENGTH = 512
20
- NUM_BEAMS = 4
21
 
22
 
23
  def get_grammar_model():
@@ -68,6 +68,56 @@ def get_grammar_model():
68
  return _grammar_model, _grammar_tokenizer, _device
69
 
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def correct_grammar(text: str) -> str:
72
  """
73
  Run grammar correction on input text using T5.
@@ -89,29 +139,36 @@ def correct_grammar(text: str) -> str:
89
  )
90
  inputs = {k: v.to(device) for k, v in inputs.items()}
91
 
92
- # Generate correction
 
 
 
 
93
  with torch.no_grad():
94
  outputs = model.generate(
95
  **inputs,
96
- max_length=MAX_OUTPUT_LENGTH,
97
  num_beams=NUM_BEAMS,
98
  early_stopping=True,
99
  no_repeat_ngram_size=3,
 
 
100
  )
101
 
102
  # Decode output
103
  corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
104
 
105
- # Safety: if model returns empty or much shorter, keep original
106
  if not corrected or not corrected.strip():
107
  return text
108
 
109
- # If the correction is drastically different (>80% changed), keep original
110
- if len(corrected) < len(text) * 0.3:
111
- logger.warning(f"Grammar output too short ({len(corrected)} vs {len(text)}), keeping original")
 
112
  return text
113
 
114
- return corrected.strip()
115
 
116
  except Exception as e:
117
  logger.error(f"Grammar correction failed: {e}")
 
3
  Lazy-loads the model on first request, keeps it resident in memory.
4
  """
5
  import logging
6
+ import re
7
  import torch
8
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
9
 
 
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():
 
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
+
120
+
121
  def correct_grammar(text: str) -> str:
122
  """
123
  Run grammar correction on input text using T5.
 
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):
169
  return text
170
 
171
+ return corrected
172
 
173
  except Exception as e:
174
  logger.error(f"Grammar correction failed: {e}")