Alexander Sanchez commited on
Commit
962adf4
·
1 Parent(s): 2c07e6b

Evaluator udpdated

Browse files
Files changed (1) hide show
  1. evaluator.py +248 -54
evaluator.py CHANGED
@@ -3,20 +3,35 @@ evaluator.py
3
  ────────────
4
  Métricas de calidad para las correcciones del sistema RAG.
5
 
6
- - CER (Character Error Rate) : nivel de carácter
7
- - WER (Word Error Rate) : nivel de palabra
8
- - Modernism score : penalización por formas modernas introducidas
9
- - Batch evaluation : evalúa el sistema sobre un conjunto de pares con GT
 
 
 
 
 
 
 
 
 
 
10
 
11
  Uso:
12
  from evaluator import Evaluator
13
  ev = Evaluator()
 
 
14
  metrics = ev.evaluate_pair(htr="...", corrected="...", gt="...")
15
- report = ev.batch_evaluate(corrector, pairs[:50])
 
 
 
16
  """
17
 
18
  import re
19
- from typing import List, Dict, Tuple
20
  from knowledge_base import GRAFIA_PATTERNS
21
 
22
 
@@ -26,19 +41,28 @@ class Evaluator:
26
 
27
  @staticmethod
28
  def cer(reference: str, hypothesis: str) -> float:
29
- """Character Error Rate (Levenshtein a nivel carácter)."""
 
 
 
 
30
  r, h = list(reference), list(hypothesis)
31
  return Evaluator._levenshtein(r, h) / max(len(r), 1)
32
 
33
  @staticmethod
34
  def wer(reference: str, hypothesis: str) -> float:
35
- """Word Error Rate (Levenshtein a nivel palabra)."""
 
 
 
 
36
  r = reference.split()
37
  h = hypothesis.split()
38
  return Evaluator._levenshtein(r, h) / max(len(r), 1)
39
 
40
  @staticmethod
41
  def _levenshtein(seq1: list, seq2: list) -> int:
 
42
  m, n = len(seq1), len(seq2)
43
  dp = list(range(n + 1))
44
  for i in range(1, m + 1):
@@ -54,60 +78,201 @@ class Evaluator:
54
  # ── Detector de modernismos ──────────────────────────────────────────────
55
 
56
  @staticmethod
57
- def modernism_penalty(original_htr: str, corrected: str) -> Dict:
58
  """
59
- Detecta formas modernas introducidas por el LLM que no estaban
60
- en el HTR original. Retorna lista de problemas detectados.
 
 
 
 
 
 
 
 
 
61
  """
62
  issues = []
63
- orig_lower = original_htr.lower()
64
  corr_lower = corrected.lower()
65
 
66
  for p in GRAFIA_PATTERNS:
67
- modern = p["modern"].lower()
68
  ancient_forms = [f.strip().lower() for f in p["ancient"].split("/")]
69
 
70
- # Si el corrected contiene la forma moderna Y el original no la tenía
71
- if modern in corr_lower and modern not in orig_lower:
72
- # Verificar que tampoco era una forma antigua válida
73
- if not any(af in orig_lower for af in ancient_forms):
 
74
  issues.append({
75
- "modern": p["modern"],
76
- "ancient": p["ancient"],
77
- "rule": p["rule"],
 
78
  })
79
 
 
80
  return {
81
  "count": len(issues),
82
  "issues": issues,
83
- "score": max(0.0, 1.0 - len(issues) * 0.1), # 0.0–1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
 
86
  # ── Evaluación de un par ─────────────────────────────────────────────────
87
 
88
- def evaluate_pair(
89
- self, htr: str, corrected: str, gt: str
90
- ) -> Dict:
91
  """
92
- Evalúa una sola corrección comparando con el groundtruth.
 
 
 
 
 
 
 
 
 
 
 
 
93
  """
94
- cer_htr = self.cer(gt, htr) # CER antes de corregir
95
- cer_corr = self.cer(gt, corrected) # CER después de corregir
96
- wer_htr = self.wer(gt, htr)
 
 
 
97
  wer_corr = self.wer(gt, corrected)
98
 
99
- modernism = self.modernism_penalty(htr, corrected)
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  return {
102
- "cer_before": round(cer_htr, 4),
103
- "cer_after": round(cer_corr, 4),
104
- "cer_improvement": round(cer_htr - cer_corr, 4),
105
- "wer_before": round(wer_htr, 4),
106
- "wer_after": round(wer_corr, 4),
107
- "wer_improvement": round(wer_htr - wer_corr, 4),
108
- "modernism": modernism,
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  # ── Evaluación en batch ──────────────────────────────────────────────────
112
 
113
  def batch_evaluate(
@@ -128,10 +293,10 @@ class Evaluator:
128
  corrected=out["corrected"],
129
  gt=pair["gt"],
130
  )
131
- metrics["id"] = pair["id"]
132
- metrics["htr"] = pair["htr"]
133
  metrics["corrected"] = out["corrected"]
134
- metrics["gt"] = pair["gt"]
135
  results.append(metrics)
136
  except Exception as e:
137
  print(f" ❌ Error en {pair['id']}: {e}")
@@ -139,25 +304,54 @@ class Evaluator:
139
  if not results:
140
  return {"error": "Sin resultados"}
141
 
142
- avg = lambda key: round(sum(r[key] for r in results) / len(results), 4)
 
 
 
 
 
 
143
 
144
  summary = {
145
- "n_evaluated": len(results),
146
- "avg_cer_before": avg("cer_before"),
147
- "avg_cer_after": avg("cer_after"),
148
- "avg_cer_improvement": avg("cer_improvement"),
149
- "avg_wer_before": avg("wer_before"),
150
- "avg_wer_after": avg("wer_after"),
151
- "avg_wer_improvement": avg("wer_improvement"),
152
- "avg_modernism_score": avg("modernism"), # via nested
153
- "detail": results,
 
 
 
 
 
 
 
 
 
 
 
 
154
  }
155
 
156
  if verbose:
157
- print(f"\n📊 RESUMEN EVALUACIÓN ({len(results)} pares)")
158
- print(f" CER: {summary['avg_cer_before']:.2%} {summary['avg_cer_after']:.2%} "
159
- f"(mejora: {summary['avg_cer_improvement']:+.2%})")
160
- print(f" WER: {summary['avg_wer_before']:.2%} {summary['avg_wer_after']:.2%} "
161
- f"(mejora: {summary['avg_wer_improvement']:+.2%})")
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  return summary
 
3
  ────────────
4
  Métricas de calidad para las correcciones del sistema RAG.
5
 
6
+ Las tres comparaciones que realiza el evaluador:
7
+
8
+ 1. GT vs HTR → error de PARTIDA (qué tan malo era el HTR)
9
+ 2. GT vs Corregido error FINAL (qué tan bueno es el RAG)
10
+ 3. HTR vs Corregido → MODERNISMOS (qué cambió el LLM, no debería modernizar)
11
+
12
+ Un cer_improvement positivo significa que el RAG mejoró el texto.
13
+ Un cer_improvement negativo significa que el LLM empeoró el texto.
14
+
15
+ Métricas:
16
+ - CER (Character Error Rate) : distancia Levenshtein a nivel carácter
17
+ - WER (Word Error Rate) : distancia Levenshtein a nivel palabra
18
+ - Modernism score : penalización por grafías s.XVI modernizadas
19
+ - Regression score : detecta si el LLM empeoró respecto al HTR
20
 
21
  Uso:
22
  from evaluator import Evaluator
23
  ev = Evaluator()
24
+
25
+ # Un solo par
26
  metrics = ev.evaluate_pair(htr="...", corrected="...", gt="...")
27
+ print(ev.format_pair_report(metrics))
28
+
29
+ # Batch
30
+ report = ev.batch_evaluate(corrector, pairs[:50])
31
  """
32
 
33
  import re
34
+ from typing import List, Dict
35
  from knowledge_base import GRAFIA_PATTERNS
36
 
37
 
 
41
 
42
  @staticmethod
43
  def cer(reference: str, hypothesis: str) -> float:
44
+ """
45
+ Character Error Rate: fracción de caracteres incorrectos.
46
+ 0.0 = perfecto, 1.0 = todo mal.
47
+ Compara: reference (GT) vs hypothesis (HTR o Corregido).
48
+ """
49
  r, h = list(reference), list(hypothesis)
50
  return Evaluator._levenshtein(r, h) / max(len(r), 1)
51
 
52
  @staticmethod
53
  def wer(reference: str, hypothesis: str) -> float:
54
+ """
55
+ Word Error Rate: fracción de palabras incorrectas.
56
+ 0.0 = perfecto, 1.0 = todo mal.
57
+ Compara: reference (GT) vs hypothesis (HTR o Corregido).
58
+ """
59
  r = reference.split()
60
  h = hypothesis.split()
61
  return Evaluator._levenshtein(r, h) / max(len(r), 1)
62
 
63
  @staticmethod
64
  def _levenshtein(seq1: list, seq2: list) -> int:
65
+ """Distancia de edición mínima entre dos secuencias."""
66
  m, n = len(seq1), len(seq2)
67
  dp = list(range(n + 1))
68
  for i in range(1, m + 1):
 
78
  # ── Detector de modernismos ──────────────────────────────────────────────
79
 
80
  @staticmethod
81
+ def modernism_penalty(htr: str, corrected: str) -> Dict:
82
  """
83
+ Comparación 3: HTR vs Corregido.
84
+
85
+ Detecta formas modernas que el LLM introdujo y que NO estaban
86
+ en el HTR original. Estas son correcciones incorrectas porque
87
+ el sistema NO debe modernizar grafías del s.XVI.
88
+
89
+ Ejemplo de error detectado:
90
+ HTR: "fizo merçed"
91
+ Corregido: "hizo merced" ← modernizó f→h y ç→c (INCORRECTO)
92
+
93
+ score: 1.0 = sin modernismos, 0.0 = muchos modernismos
94
  """
95
  issues = []
96
+ htr_lower = htr.lower()
97
  corr_lower = corrected.lower()
98
 
99
  for p in GRAFIA_PATTERNS:
100
+ modern = p["modern"].lower()
101
  ancient_forms = [f.strip().lower() for f in p["ancient"].split("/")]
102
 
103
+ # El corregido tiene la forma moderna Y el HTR no la tenía
104
+ if modern in corr_lower and modern not in htr_lower:
105
+ # Además el HTR tampoco tenía la forma antigua
106
+ # (si la tenía, es una expansión de abreviatura válida)
107
+ if not any(af in htr_lower for af in ancient_forms):
108
  issues.append({
109
+ "modern": p["modern"],
110
+ "ancient": p["ancient"],
111
+ "rule": p["rule"],
112
+ "category": p.get("category", ""),
113
  })
114
 
115
+ score = max(0.0, 1.0 - len(issues) * 0.1)
116
  return {
117
  "count": len(issues),
118
  "issues": issues,
119
+ "score": round(score, 4),
120
+ }
121
+
122
+ # ── Detector de regresiones ──────────────────────────────────────────────
123
+
124
+ @staticmethod
125
+ def regression_check(htr: str, corrected: str, gt: str) -> Dict:
126
+ """
127
+ Detecta si el LLM empeoró el texto respecto al HTR original.
128
+
129
+ Un resultado positivo en cer_improvement no garantiza que todo
130
+ esté bien — el LLM podría haber corregido unos errores e
131
+ introducido otros. Esta función identifica palabras que estaban
132
+ bien en el HTR y el LLM cambió incorrectamente.
133
+ """
134
+ htr_words = htr.split()
135
+ corr_words = corrected.split()
136
+ gt_words = gt.split()
137
+
138
+ regressions = []
139
+ # Comparar palabra a palabra hasta el mínimo de las tres listas
140
+ for i, gt_w in enumerate(gt_words):
141
+ htr_w = htr_words[i] if i < len(htr_words) else ""
142
+ corr_w = corr_words[i] if i < len(corr_words) else ""
143
+
144
+ # El HTR estaba bien, el corregido está mal
145
+ if htr_w == gt_w and corr_w != gt_w:
146
+ regressions.append({
147
+ "position": i,
148
+ "gt": gt_w,
149
+ "htr": htr_w, # correcto en HTR
150
+ "corrected":corr_w, # empeorado por el LLM
151
+ })
152
+
153
+ return {
154
+ "count": len(regressions),
155
+ "regressions": regressions,
156
  }
157
 
158
  # ── Evaluación de un par ─────────────────────────────────────────────────
159
 
160
+ def evaluate_pair(self, htr: str, corrected: str, gt: str) -> Dict:
 
 
161
  """
162
+ Evalúa una corrección con las tres comparaciones:
163
+
164
+ Comparación 1 — GT vs HTR:
165
+ Mide el error de partida. Cuánto se alejaba el HTR del GT.
166
+
167
+ Comparación 2 — GT vs Corregido:
168
+ Mide el error final. Cuánto se aleja la corrección del GT.
169
+ cer_improvement > 0 → el RAG mejoró el texto
170
+ cer_improvement < 0 → el RAG empeoró el texto
171
+
172
+ Comparación 3 — HTR vs Corregido:
173
+ Detecta modernismos introducidos por el LLM.
174
+ El LLM NO debe cambiar grafías válidas del s.XVI.
175
  """
176
+ # ── Comparación 1: GT vs HTR (error de partida) ────────────────────
177
+ cer_htr = self.cer(gt, htr)
178
+ wer_htr = self.wer(gt, htr)
179
+
180
+ # ── Comparación 2: GT vs Corregido (error final) ───────────────────
181
+ cer_corr = self.cer(gt, corrected)
182
  wer_corr = self.wer(gt, corrected)
183
 
184
+ cer_improvement = cer_htr - cer_corr # positivo = mejoró
185
+ wer_improvement = wer_htr - wer_corr
186
+
187
+ # Veredicto de la corrección
188
+ if cer_improvement > 0.02:
189
+ verdict = "✓ MEJORADO"
190
+ elif cer_improvement < -0.02:
191
+ verdict = "✗ EMPEORADO"
192
+ else:
193
+ verdict = "~ SIN CAMBIO SIGNIFICATIVO"
194
+
195
+ # ── Comparación 3: HTR vs Corregido (modernismos) ─────────────────
196
+ modernism = self.modernism_penalty(htr, corrected)
197
+ regression = self.regression_check(htr, corrected, gt)
198
 
199
  return {
200
+ # ── Error de partida (GT vs HTR) ──────────────────────────────
201
+ "cer_before": round(cer_htr, 4),
202
+ "wer_before": round(wer_htr, 4),
203
+
204
+ # ── Error final (GT vs Corregido) ─────────────────────────────
205
+ "cer_after": round(cer_corr, 4),
206
+ "wer_after": round(wer_corr, 4),
207
+
208
+ # ── Mejora neta ───────────────────────────────────────────────
209
+ "cer_improvement": round(cer_improvement, 4),
210
+ "wer_improvement": round(wer_improvement, 4),
211
+ "verdict": verdict,
212
+
213
+ # ── Modernismos (HTR vs Corregido) ────────────────────────────
214
+ "modernism_score": modernism["score"],
215
+ "modernism": modernism,
216
+
217
+ # ── Regresiones ───────────────────────────────────────────────
218
+ "regression": regression,
219
  }
220
 
221
+ # ── Reporte legible de un par ────────────────────────────────────────────
222
+
223
+ @staticmethod
224
+ def format_pair_report(metrics: Dict) -> str:
225
+ """
226
+ Formatea las métricas de un par en texto legible para la UI.
227
+ """
228
+ lines = []
229
+ lines.append("─" * 50)
230
+ lines.append("📊 EVALUACIÓN DE LA CORRECCIÓN")
231
+ lines.append("─" * 50)
232
+
233
+ # Comparación 1 — error de partida
234
+ lines.append("\n① Error de partida (GT vs HTR original)")
235
+ lines.append(f" CER: {metrics['cer_before']:.2%} WER: {metrics['wer_before']:.2%}")
236
+
237
+ # Comparación 2 — error final
238
+ lines.append("\n② Error final (GT vs Texto corregido)")
239
+ lines.append(f" CER: {metrics['cer_after']:.2%} WER: {metrics['wer_after']:.2%}")
240
+
241
+ # Mejora neta
242
+ cer_imp = metrics["cer_improvement"]
243
+ wer_imp = metrics["wer_improvement"]
244
+ sign_c = "+" if cer_imp >= 0 else ""
245
+ sign_w = "+" if wer_imp >= 0 else ""
246
+ lines.append(f"\n Mejora CER: {sign_c}{cer_imp:.2%} Mejora WER: {sign_w}{wer_imp:.2%}")
247
+ lines.append(f" {metrics['verdict']}")
248
+
249
+ # Comparación 3 — modernismos
250
+ mod = metrics["modernism"]
251
+ lines.append(f"\n③ Modernismos introducidos (HTR vs Corregido)")
252
+ if mod["count"] == 0:
253
+ lines.append(" ✓ Ninguno — el LLM respetó las grafías del s.XVI")
254
+ else:
255
+ lines.append(f" ✗ {mod['count']} modernismo(s) detectado(s):")
256
+ for issue in mod["issues"]:
257
+ lines.append(
258
+ f" • '{issue['modern']}' introducido "
259
+ f"(debería ser '{issue['ancient']}'): {issue['rule']}"
260
+ )
261
+
262
+ # Regresiones
263
+ reg = metrics["regression"]
264
+ if reg["count"] > 0:
265
+ lines.append(f"\n⚠ Regresiones: {reg['count']} palabra(s) correctas en HTR empeoradas por el LLM:")
266
+ for r in reg["regressions"][:5]:
267
+ lines.append(
268
+ f" • posición {r['position']}: "
269
+ f"HTR='{r['htr']}' → Corregido='{r['corrected']}' "
270
+ f"(GT='{r['gt']}')"
271
+ )
272
+
273
+ lines.append("─" * 50)
274
+ return "\n".join(lines)
275
+
276
  # ── Evaluación en batch ──────────────────────────────────────────────────
277
 
278
  def batch_evaluate(
 
293
  corrected=out["corrected"],
294
  gt=pair["gt"],
295
  )
296
+ metrics["id"] = pair["id"]
297
+ metrics["htr"] = pair["htr"]
298
  metrics["corrected"] = out["corrected"]
299
+ metrics["gt"] = pair["gt"]
300
  results.append(metrics)
301
  except Exception as e:
302
  print(f" ❌ Error en {pair['id']}: {e}")
 
304
  if not results:
305
  return {"error": "Sin resultados"}
306
 
307
+ def avg(key):
308
+ return round(sum(r[key] for r in results) / len(results), 4)
309
+
310
+ n = len(results)
311
+ mejoras = sum(1 for r in results if r["cer_improvement"] > 0.02)
312
+ empeorados = sum(1 for r in results if r["cer_improvement"] < -0.02)
313
+ sin_cambio = n - mejoras - empeorados
314
 
315
  summary = {
316
+ "n_evaluated": n,
317
+
318
+ # ── Comparación 1: GT vs HTR ──────────────────────────────────
319
+ "avg_cer_before": avg("cer_before"),
320
+ "avg_wer_before": avg("wer_before"),
321
+
322
+ # ── Comparación 2: GT vs Corregido ────────────────────────────
323
+ "avg_cer_after": avg("cer_after"),
324
+ "avg_wer_after": avg("wer_after"),
325
+
326
+ # ── Mejora neta ───────────────────────────────────────────────
327
+ "avg_cer_improvement": avg("cer_improvement"),
328
+ "avg_wer_improvement": avg("wer_improvement"),
329
+ "n_mejorados": mejoras,
330
+ "n_empeorados": empeorados,
331
+ "n_sin_cambio": sin_cambio,
332
+
333
+ # ── Comparación 3: Modernismos ────────────────────────────────
334
+ "avg_modernism_score": avg("modernism_score"),
335
+
336
+ "detail": results,
337
  }
338
 
339
  if verbose:
340
+ print(f"\n{'─'*55}")
341
+ print(f"📊 RESUMEN EVALUACIÓN ({n} pares)")
342
+ print(f"{''*55}")
343
+ print(f"\n① Error de partida (GT vs HTR):")
344
+ print(f" CER: {summary['avg_cer_before']:.2%} WER: {summary['avg_wer_before']:.2%}")
345
+ print(f"\n② Error final (GT vs Corregido):")
346
+ print(f" CER: {summary['avg_cer_after']:.2%} WER: {summary['avg_wer_after']:.2%}")
347
+ print(f"\n Mejora CER: {summary['avg_cer_improvement']:+.2%} "
348
+ f"Mejora WER: {summary['avg_wer_improvement']:+.2%}")
349
+ print(f"\n Mejorados : {mejoras}/{n} ({mejoras/n:.0%})")
350
+ print(f" Empeorados : {empeorados}/{n} ({empeorados/n:.0%})")
351
+ print(f" Sin cambio : {sin_cambio}/{n} ({sin_cambio/n:.0%})")
352
+ print(f"\n③ Modernismos (HTR vs Corregido):")
353
+ print(f" Score promedio: {summary['avg_modernism_score']:.2%} "
354
+ f"(1.0 = sin modernismos)")
355
+ print(f"{'─'*55}")
356
 
357
  return summary