From 07a7ba8c01dee860b899466b58bdf9ca0d1b08ea Mon Sep 17 00:00:00 2001 From: Sergio Jimenez Jimenez Date: Mon, 19 Jan 2026 17:38:43 +0100 Subject: [PATCH] regen docs --- docs/03_objetivos_metodologia.md | 86 ++----- docs/04_desarrollo_especifico.md | 280 ++++------------------ docs/05_conclusiones_trabajo_futuro.md | 2 +- thesis_output/plantilla_individual.htm | 311 +++++-------------------- 4 files changed, 138 insertions(+), 541 deletions(-) diff --git a/docs/03_objetivos_metodologia.md b/docs/03_objetivos_metodologia.md index cdff727..a6d2541 100644 --- a/docs/03_objetivos_metodologia.md +++ b/docs/03_objetivos_metodologia.md @@ -104,16 +104,7 @@ flowchart LR #### Clase ImageTextDataset -Se implementó una clase Python para cargar pares imagen-texto: - -```python -class ImageTextDataset: - def __init__(self, root): - # Carga pares (imagen, texto) de carpetas pareadas - - def __getitem__(self, idx): - # Retorna (PIL.Image, str) -``` +Se implementó una clase Python para cargar pares imagen-texto que retorna tuplas (PIL.Image, str) desde carpetas pareadas. La implementación completa está disponible en `src/ocr_benchmark_notebook.ipynb` (ver Anexo A). ### Fase 2: Benchmark Comparativo @@ -131,17 +122,7 @@ class ImageTextDataset: #### Métricas de Evaluación -Se utilizó la biblioteca `jiwer` para calcular: - -```python -from jiwer import wer, cer - -def evaluate_text(reference, prediction): - return { - 'WER': wer(reference, prediction), - 'CER': cer(reference, prediction) - } -``` +Se utilizó la biblioteca `jiwer` para calcular CER y WER comparando el texto de referencia con la predicción del modelo OCR. La implementación está disponible en `src/ocr_benchmark_notebook.ipynb` (ver Anexo A). ### Fase 3: Espacio de Búsqueda @@ -163,66 +144,45 @@ def evaluate_text(reference, prediction): #### Configuración de Ray Tune -```python -from ray import tune -from ray.tune.search.optuna import OptunaSearch - -search_space = { - "use_doc_orientation_classify": tune.choice([True, False]), - "use_doc_unwarping": tune.choice([True, False]), - "textline_orientation": tune.choice([True, False]), - "text_det_thresh": tune.uniform(0.0, 0.7), - "text_det_box_thresh": tune.uniform(0.0, 0.7), - "text_det_unclip_ratio": tune.choice([0.0]), - "text_rec_score_thresh": tune.uniform(0.0, 0.7), -} - -tuner = tune.Tuner( - trainable_paddle_ocr, - tune_config=tune.TuneConfig( - metric="CER", - mode="min", - search_alg=OptunaSearch(), - num_samples=64, - max_concurrent_trials=2 - ) -) -``` +El espacio de búsqueda se definió utilizando `tune.choice()` para parámetros booleanos y `tune.uniform()` para parámetros continuos, con OptunaSearch como algoritmo de optimización configurado para minimizar CER en 64 trials. La implementación completa está disponible en `src/raytune/raytune_ocr.py` (ver Anexo A). ### Fase 4: Ejecución de Optimización #### Arquitectura de Ejecución -Debido a incompatibilidades entre Ray y PaddleOCR en el mismo proceso, se implementó una arquitectura basada en subprocesos: +Se implementó una arquitectura basada en contenedores Docker para aislar los servicios OCR y facilitar la reproducibilidad: ```mermaid --- -title: "Arquitectura de ejecución con subprocesos" +title: "Arquitectura de ejecución con Docker Compose" --- flowchart LR - A["Ray Tune (proceso principal)"] + subgraph Docker["Docker Compose"] + A["RayTune Container"] + B["OCR Service Container"] + end - A --> B["Subprocess 1: paddle_ocr_tuning.py --config"] - B --> B_out["Retorna JSON con métricas"] - - A --> C["Subprocess 2: paddle_ocr_tuning.py --config"] - C --> C_out["Retorna JSON con métricas"] + A -->|"HTTP POST /evaluate"| B + B -->|"JSON {CER, WER, TIME}"| A + A -.->|"Health check /health"| B ``` -#### Script de Evaluación (paddle_ocr_tuning.py) +#### Ejecución con Docker Compose -El script recibe hiperparámetros por línea de comandos: +Los servicios se orquestan mediante Docker Compose (`src/docker-compose.tuning.*.yml`): ```bash -python paddle_ocr_tuning.py \ - --pdf-folder ./dataset \ - --textline-orientation True \ - --text-det-box-thresh 0.5 \ - --text-det-thresh 0.4 \ - --text-rec-score-thresh 0.6 +# Iniciar servicio OCR +docker compose -f docker-compose.tuning.doctr.yml up -d doctr-gpu + +# Ejecutar optimización (64 trials) +docker compose -f docker-compose.tuning.doctr.yml run raytune --service doctr --samples 64 + +# Detener servicios +docker compose -f docker-compose.tuning.doctr.yml down ``` -Y retorna métricas en formato JSON: +El servicio OCR expone una API REST que retorna métricas en formato JSON: ```json { diff --git a/docs/04_desarrollo_especifico.md b/docs/04_desarrollo_especifico.md index 0241b3b..d1064fe 100644 --- a/docs/04_desarrollo_especifico.md +++ b/docs/04_desarrollo_especifico.md @@ -70,121 +70,25 @@ Se utilizó el documento "Instrucciones para la redacción y elaboración del TF #### Proceso de Conversión -La conversión del PDF a imágenes se realizó mediante PyMuPDF (fitz): - -```python -import fitz # PyMuPDF - -def pdf_to_images(pdf_path, output_dir, dpi=300): - doc = fitz.open(pdf_path) - for page_num, page in enumerate(doc): - # Matriz de transformación para 300 DPI - mat = fitz.Matrix(dpi/72, dpi/72) - pix = page.get_pixmap(matrix=mat) - pix.save(f"{output_dir}/page_{page_num:04d}.png") -``` - -La resolución de 300 DPI fue seleccionada como estándar para OCR de documentos, proporcionando suficiente detalle para caracteres pequeños sin generar archivos excesivamente grandes. +La conversión del PDF a imágenes se realizó mediante PyMuPDF (fitz) a 300 DPI, resolución estándar para OCR que proporciona suficiente detalle para caracteres pequeños sin generar archivos excesivamente grandes. La implementación está disponible en `src/ocr_benchmark_notebook.ipynb` (ver Anexo A). #### Extracción del Ground Truth -El texto de referencia se extrajo directamente del PDF mediante PyMuPDF: - -```python -def extract_text(pdf_path): - doc = fitz.open(pdf_path) - text = "" - for page in doc: - blocks = page.get_text("dict")["blocks"] - for block in blocks: - if "lines" in block: - for line in block["lines"]: - for span in line["spans"]: - text += span["text"] - text += "\n" - return text -``` - -Esta aproximación preserva la estructura de líneas del documento original, aunque puede introducir errores en layouts muy complejos (tablas anidadas, texto en columnas). +El texto de referencia se extrajo directamente del PDF mediante PyMuPDF, preservando la estructura de líneas del documento original. Esta aproximación puede introducir errores en layouts muy complejos (tablas anidadas, texto en columnas). La implementación está disponible en `src/ocr_benchmark_notebook.ipynb` (ver Anexo A). #### Configuración de los Modelos -Según el código en `ocr_benchmark_notebook.ipynb`: +La configuración de cada modelo se detalla en `src/ocr_benchmark_notebook.ipynb` (ver Anexo A): -**EasyOCR**: -```python -import easyocr +- **EasyOCR**: Configurado con soporte para español e inglés, permitiendo reconocer palabras en ambos idiomas que puedan aparecer en documentos académicos (referencias, términos técnicos). -easyocr_reader = easyocr.Reader(['es', 'en']) # Spanish and English -results = easyocr_reader.readtext(image_path) -text = ' '.join([r[1] for r in results]) -``` +- **PaddleOCR (PP-OCRv5)**: Se utilizaron los modelos "server" (PP-OCRv5_server_det y PP-OCRv5_server_rec) que ofrecen mayor precisión a costa de mayor tiempo de inferencia. La versión utilizada fue PaddleOCR 3.2.0. -La configuración incluye soporte para español e inglés, permitiendo reconocer palabras en ambos idiomas que puedan aparecer en documentos académicos (referencias, términos técnicos). - -**PaddleOCR (PP-OCRv5)**: -```python -from paddleocr import PaddleOCR - -paddleocr_model = PaddleOCR( - text_detection_model_name="PP-OCRv5_server_det", - text_recognition_model_name="PP-OCRv5_server_rec", - use_doc_orientation_classify=False, - use_doc_unwarping=False, - use_textline_orientation=True, -) - -result = paddleocr_model.predict(image_path) -text = '\n'.join([line['rec_texts'][0] for line in result[0]['rec_res']]) -``` - -Se utilizaron los modelos "server" que ofrecen mayor precisión a costa de mayor tiempo de inferencia. La versión utilizada fue PaddleOCR 3.2.0. - -**DocTR**: -```python -from doctr.models import ocr_predictor - -doctr_model = ocr_predictor( - det_arch="db_resnet50", - reco_arch="sar_resnet31", - pretrained=True -) - -result = doctr_model([image]) -text = result.render() -``` - -Se seleccionaron las arquitecturas db_resnet50 para detección y sar_resnet31 para reconocimiento, representando una configuración de alta precisión. +- **DocTR**: Se seleccionaron las arquitecturas db_resnet50 para detección y sar_resnet31 para reconocimiento, representando una configuración de alta precisión. #### Métricas de Evaluación -Se utilizó la biblioteca `jiwer` para calcular CER y WER de manera estandarizada: - -```python -from jiwer import wer, cer - -def evaluate_text(reference, prediction): - """ - Calcula métricas de error entre texto de referencia y predicción. - - Args: - reference: Texto ground truth - prediction: Texto predicho por el OCR - - Returns: - dict con WER y CER - """ - # Normalización básica - ref_clean = reference.lower().strip() - pred_clean = prediction.lower().strip() - - return { - 'WER': wer(ref_clean, pred_clean), - 'CER': cer(ref_clean, pred_clean) - } -``` - -La normalización a minúsculas y eliminación de espacios extremos asegura una comparación justa que no penaliza diferencias de capitalización. +Se utilizó la biblioteca `jiwer` para calcular CER y WER de manera estandarizada. La normalización a minúsculas y eliminación de espacios extremos asegura una comparación justa que no penaliza diferencias de capitalización. La implementación está disponible en `src/ocr_benchmark_notebook.ipynb` (ver Anexo A). ### Resultados del Benchmark @@ -358,38 +262,47 @@ El experimento se ejecutó en el siguiente entorno: #### Arquitectura de Ejecución -Debido a incompatibilidades entre Ray y PaddleOCR cuando se ejecutan en el mismo proceso, se implementó una arquitectura basada en subprocesos: +La arquitectura basada en contenedores Docker es fundamental para este proyecto debido a los conflictos de dependencias inherentes entre los diferentes componentes: + +- **Conflictos entre motores OCR**: PaddleOCR, DocTR y EasyOCR tienen dependencias mutuamente incompatibles (diferentes versiones de PyTorch/PaddlePaddle, OpenCV, etc.) +- **Incompatibilidades CUDA/cuDNN**: Cada motor OCR requiere versiones específicas de CUDA y cuDNN que no pueden coexistir en un mismo entorno virtual +- **Aislamiento de Ray Tune**: Ray Tune tiene sus propias dependencias que pueden entrar en conflicto con las librerías de inferencia OCR + +Esta arquitectura containerizada permite ejecutar cada componente en su entorno aislado óptimo, comunicándose via API REST: ```mermaid --- -title: "Arquitectura de ejecución con subprocesos" +title: "Arquitectura de ejecución con Docker Compose" --- flowchart LR - A["Ray Tune (proceso principal)"] + subgraph Docker["Docker Compose"] + A["RayTune Container"] + B["OCR Service Container"] + end - A --> B["Subprocess 1: paddle_ocr_tuning.py --config"] - B --> B_out["Retorna JSON con métricas"] - - A --> C["Subprocess 2: paddle_ocr_tuning.py --config"] - C --> C_out["Retorna JSON con métricas"] + A -->|"HTTP POST /evaluate"| B + B -->|"JSON {CER, WER, TIME}"| A + A -.->|"Health check /health"| B ``` -El script `src/paddle_ocr_tuning.py` actúa como wrapper que: -1. Recibe hiperparámetros por línea de comandos -2. Inicializa PaddleOCR con la configuración especificada -3. Evalúa sobre el dataset -4. Retorna métricas en formato JSON +La arquitectura containerizada (`src/docker-compose.tuning.*.yml`) ofrece: +1. Aislamiento de dependencias entre Ray Tune y los motores OCR +2. Health checks automáticos para asegurar disponibilidad del servicio +3. Comunicación via API REST (endpoints `/health` y `/evaluate`) +4. Soporte para GPU mediante nvidia-docker ```bash -python paddle_ocr_tuning.py \ - --pdf-folder ./dataset \ - --textline-orientation True \ - --text-det-box-thresh 0.5 \ - --text-det-thresh 0.4 \ - --text-rec-score-thresh 0.6 +# Iniciar servicio OCR con GPU +docker compose -f docker-compose.tuning.doctr.yml up -d doctr-gpu + +# Ejecutar optimización (64 trials) +docker compose -f docker-compose.tuning.doctr.yml run raytune --service doctr --samples 64 + +# Detener servicios +docker compose -f docker-compose.tuning.doctr.yml down ``` -Salida: +Respuesta del servicio OCR: ```json { "CER": 0.0125, @@ -416,54 +329,11 @@ Para la fase de optimización se extendió el dataset: *Fuente: Elaboración propia.* -La clase `ImageTextDataset` en `src/dataset_manager.py` gestiona la carga de pares imagen-texto: - -```python -class ImageTextDataset: - def __init__(self, root): - """ - Carga pares (imagen, texto) de carpetas pareadas. - - Estructura esperada: - root/ - 0/ - img/ - page_0001.png - txt/ - page_0001.txt - """ - self.pairs = [] - for doc_folder in sorted(os.listdir(root)): - img_folder = os.path.join(root, doc_folder, 'img') - txt_folder = os.path.join(root, doc_folder, 'txt') - # Cargar pares... - - def __getitem__(self, idx): - img_path, txt_path = self.pairs[idx] - return PIL.Image.open(img_path), open(txt_path).read() -``` +La clase `ImageTextDataset` gestiona la carga de pares imagen-texto desde la estructura de carpetas pareadas. La implementación está disponible en el repositorio (ver Anexo A). #### Espacio de Búsqueda -El espacio de búsqueda se definió considerando los hiperparámetros más relevantes identificados en la documentación de PaddleOCR: - -```python -from ray import tune -from ray.tune.search.optuna import OptunaSearch - -search_space = { - # Parámetros booleanos - "use_doc_orientation_classify": tune.choice([True, False]), - "use_doc_unwarping": tune.choice([True, False]), - "textline_orientation": tune.choice([True, False]), - - # Parámetros continuos (umbrales) - "text_det_thresh": tune.uniform(0.0, 0.7), - "text_det_box_thresh": tune.uniform(0.0, 0.7), - "text_det_unclip_ratio": tune.choice([0.0]), # Fijado - "text_rec_score_thresh": tune.uniform(0.0, 0.7), -} -``` +El espacio de búsqueda se definió considerando los hiperparámetros más relevantes identificados en la documentación de PaddleOCR, utilizando `tune.choice()` para parámetros booleanos y `tune.uniform()` para umbrales continuos. La implementación está disponible en `src/raytune/raytune_ocr.py` (ver Anexo A). **Tabla 17.** *Descripción detallada del espacio de búsqueda.* @@ -489,23 +359,7 @@ search_space = { #### Configuración de Ray Tune -```python -tuner = tune.Tuner( - trainable_paddle_ocr, - tune_config=tune.TuneConfig( - metric="CER", - mode="min", - search_alg=OptunaSearch(), - num_samples=64, - max_concurrent_trials=2 - ), - run_config=air.RunConfig( - verbose=2, - log_to_file=False - ), - param_space=search_space -) -``` +Se configuró Ray Tune con OptunaSearch como algoritmo de búsqueda, optimizando CER en 64 trials con 2 ejecuciones concurrentes. La implementación está disponible en `src/raytune/raytune_ocr.py` (ver Anexo A). **Tabla 18.** *Parámetros de configuración de Ray Tune.* @@ -711,33 +565,7 @@ Los trials con CER muy alto (>20%) presentaron patrones específicos: #### Evaluación sobre Dataset Completo -La configuración óptima identificada se evaluó sobre el dataset completo de 24 páginas, comparando con la configuración baseline: - -**Configuración Baseline:** -```python -baseline_config = { - "textline_orientation": False, # Valor por defecto - "use_doc_orientation_classify": False, - "use_doc_unwarping": False, - "text_det_thresh": 0.3, # Valor por defecto - "text_det_box_thresh": 0.6, # Valor por defecto - "text_det_unclip_ratio": 1.5, # Valor por defecto - "text_rec_score_thresh": 0.5, # Valor por defecto -} -``` - -**Configuración Optimizada:** -```python -optimized_config = { - "textline_orientation": True, - "use_doc_orientation_classify": False, - "use_doc_unwarping": False, - "text_det_thresh": 0.4690, - "text_det_box_thresh": 0.5412, - "text_det_unclip_ratio": 0.0, - "text_rec_score_thresh": 0.6350, -} -``` +La configuración óptima identificada se evaluó sobre el dataset completo de 24 páginas, comparando con la configuración baseline (valores por defecto de PaddleOCR). Los parámetros optimizados más relevantes fueron: `textline_orientation=True`, `text_det_thresh=0.4690`, `text_det_box_thresh=0.5412`, y `text_rec_score_thresh=0.6350`. **Tabla 27.** *Comparación baseline vs optimizado (24 páginas).* @@ -813,7 +641,7 @@ xychart-beta Esta sección ha presentado: -1. **Configuración del experimento**: Arquitectura de subprocesos, dataset extendido, espacio de búsqueda de 7 dimensiones +1. **Configuración del experimento**: Arquitectura Docker Compose, dataset extendido, espacio de búsqueda de 7 dimensiones 2. **Resultados estadísticos**: - CER medio: 5.25% (std: 11.03%) @@ -1007,24 +835,18 @@ Para documentos PDF digitales como los evaluados, estos módulos son innecesario Para documentos académicos en español similares a los evaluados: -**Configuración recomendada:** -```python -config_recomendada = { - # OBLIGATORIO - "textline_orientation": True, +**Tabla 31.** *Configuración recomendada para PaddleOCR.* - # RECOMENDADO - "text_det_thresh": 0.45, # Rango: 0.4-0.5 - "text_rec_score_thresh": 0.6, # Rango: 0.5-0.7 +| Parámetro | Valor | Prioridad | Justificación | +|-----------|-------|-----------|---------------| +| `textline_orientation` | True | Obligatorio | Reduce CER en 69.7% | +| `text_det_thresh` | 0.45 (rango: 0.4-0.5) | Recomendado | Correlación fuerte con CER | +| `text_rec_score_thresh` | 0.6 (rango: 0.5-0.7) | Recomendado | Filtra reconocimientos poco confiables | +| `text_det_box_thresh` | 0.55 (rango: 0.5-0.6) | Opcional | Impacto moderado | +| `use_doc_orientation_classify` | False | No recomendado | Innecesario para PDFs digitales | +| `use_doc_unwarping` | False | No recomendado | Innecesario para PDFs digitales | - # OPCIONAL - "text_det_box_thresh": 0.55, # Rango: 0.5-0.6 - - # NO RECOMENDADO para PDFs digitales - "use_doc_orientation_classify": False, - "use_doc_unwarping": False, -} -``` +*Fuente: Análisis de resultados de optimización.* #### Cuándo Aplicar Esta Metodología diff --git a/docs/05_conclusiones_trabajo_futuro.md b/docs/05_conclusiones_trabajo_futuro.md index 5d953e4..b19a1c5 100644 --- a/docs/05_conclusiones_trabajo_futuro.md +++ b/docs/05_conclusiones_trabajo_futuro.md @@ -38,7 +38,7 @@ El objetivo principal del trabajo era alcanzar un CER inferior al 2% en document **Respecto a OE4 (Optimización con Ray Tune)**: - Se ejecutaron 64 trials con el algoritmo OptunaSearch - El tiempo total del experimento fue aproximadamente 6 horas (en CPU) -- La arquitectura basada en subprocesos permitió superar incompatibilidades entre Ray y PaddleOCR +- La arquitectura basada en contenedores Docker permitió superar incompatibilidades entre Ray y los motores OCR, facilitando además la portabilidad y reproducibilidad **Respecto a OE5 (Validación de la configuración)**: - Se validó la configuración óptima sobre el dataset completo de 24 páginas diff --git a/thesis_output/plantilla_individual.htm b/thesis_output/plantilla_individual.htm index 4b121c9..a470396 100644 --- a/thesis_output/plantilla_individual.htm +++ b/thesis_output/plantilla_individual.htm @@ -4982,13 +4982,7 @@ concretos y metodología de trabajoFuente: Elaboración propia.

 

Clase ImageTextDataset

-

Se implementó una clase Python para cargar pares imagen-texto:

-

class ImageTextDataset:
-    def __init__(self, root):
-        # Carga pares (imagen, texto) de carpetas pareadas
-
-    def __getitem__(self, idx):
-        # Retorna (PIL.Image, str)

+

Se implementó una clase Python para cargar pares imagen-texto que retorna tuplas (PIL.Image, str) desde carpetas pareadas. La implementación completa está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).

Fase 2: Benchmark Comparativo

Modelos Evaluados

Tabla 14. Modelos OCR evaluados en el benchmark inicial.

@@ -4996,14 +4990,7 @@ concretos y metodología de trabajoFuente: Elaboración propia.

 

Métricas de Evaluación

-

Se utilizó la biblioteca jiwer para calcular:

-

from jiwer import wer, cer
-
-def evaluate_text(reference, prediction):
-    return {
-        'WER': wer(reference, prediction),
-        'CER': cer(reference, prediction)
-    }

+

Se utilizó la biblioteca jiwer para calcular CER y WER comparando el texto de referencia con la predicción del modelo OCR. La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).

Fase 3: Espacio de Búsqueda

Hiperparámetros Seleccionados

Tabla 15. Hiperparámetros seleccionados para optimización.

@@ -5011,45 +4998,25 @@ def evaluate_text(reference, prediction):

Fuente: Elaboración propia.

 

Configuración de Ray Tune

-

from ray import tune
-from ray.tune.search.optuna import OptunaSearch
-
-search_space = {
-    "use_doc_orientation_classify": tune.choice([True, False]),
-    "use_doc_unwarping": tune.choice([True, False]),
-    "textline_orientation": tune.choice([True, False]),
-    "text_det_thresh": tune.uniform(0.0, 0.7),
-    "text_det_box_thresh": tune.uniform(0.0, 0.7),
-    "text_det_unclip_ratio": tune.choice([0.0]),
-    "text_rec_score_thresh": tune.uniform(0.0, 0.7),
-}
-
-tuner = tune.Tuner(
-    trainable_paddle_ocr,
-    tune_config=tune.TuneConfig(
-        metric="CER",
-        mode="min",
-        search_alg=OptunaSearch(),
-        num_samples=64,
-        max_concurrent_trials=2
-    )
-)

+

El espacio de búsqueda se definió utilizando tune.choice() para parámetros booleanos y tune.uniform() para parámetros continuos, con OptunaSearch como algoritmo de optimización configurado para minimizar CER en 64 trials. La implementación completa está disponible en src/raytune/raytune_ocr.py (ver Anexo A).

Fase 4: Ejecución de Optimización

Arquitectura de Ejecución

-

Debido a incompatibilidades entre Ray y PaddleOCR en el mismo proceso, se implementó una arquitectura basada en subprocesos:

-

Figura 5. Arquitectura de ejecución con subprocesos

-

Arquitectura de ejecución con subprocesos

+

Se implementó una arquitectura basada en contenedores Docker para aislar los servicios OCR y facilitar la reproducibilidad:

+

Figura 5. Arquitectura de ejecución con Docker Compose

+

Arquitectura de ejecución con Docker Compose

Fuente: Elaboración propia.

 

-

Script de Evaluación (paddle_ocr_tuning.py)

-

El script recibe hiperparámetros por línea de comandos:

-

python paddle_ocr_tuning.py \
-    --pdf-folder ./dataset \
-    --textline-orientation True \
-    --text-det-box-thresh 0.5 \
-    --text-det-thresh 0.4 \
-    --text-rec-score-thresh 0.6

-

Y retorna métricas en formato JSON:

+

Ejecución con Docker Compose

+

Los servicios se orquestan mediante Docker Compose (src/docker-compose.tuning.*.yml):

+

# Iniciar servicio OCR
+docker compose -f docker-compose.tuning.doctr.yml up -d doctr-gpu
+
+# Ejecutar optimización (64 trials)
+docker compose -f docker-compose.tuning.doctr.yml run raytune --service doctr --samples 64
+
+# Detener servicios
+docker compose -f docker-compose.tuning.doctr.yml down

+

El servicio OCR expone una API REST que retorna métricas en formato JSON:

{
     "CER": 0.0125,
     "WER": 0.1040,
@@ -5142,91 +5109,16 @@ color:#0098CD;mso-font-kerning:16.0pt;mso-bidi-font-weight:bold'>Fuente: Elaboración propia.

 

Proceso de Conversión

-

La conversión del PDF a imágenes se realizó mediante PyMuPDF (fitz):

-

import fitz  # PyMuPDF
-
-def pdf_to_images(pdf_path, output_dir, dpi=300):
-    doc = fitz.open(pdf_path)
-    for page_num, page in enumerate(doc):
-        # Matriz de transformación para 300 DPI
-        mat = fitz.Matrix(dpi/72, dpi/72)
-        pix = page.get_pixmap(matrix=mat)
-        pix.save(f"{output_dir}/page_{page_num:04d}.png")

-

La resolución de 300 DPI fue seleccionada como estándar para OCR de documentos, proporcionando suficiente detalle para caracteres pequeños sin generar archivos excesivamente grandes.

+

La conversión del PDF a imágenes se realizó mediante PyMuPDF (fitz) a 300 DPI, resolución estándar para OCR que proporciona suficiente detalle para caracteres pequeños sin generar archivos excesivamente grandes. La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).

Extracción del Ground Truth

-

El texto de referencia se extrajo directamente del PDF mediante PyMuPDF:

-

def extract_text(pdf_path):
-    doc = fitz.open(pdf_path)
-    text = ""
-    for page in doc:
-        blocks = page.get_text("dict")["blocks"]
-        for block in blocks:
-            if "lines" in block:
-                for line in block["lines"]:
-                    for span in line["spans"]:
-                        text += span["text"]
-                    text += "\n"
-    return text

-

Esta aproximación preserva la estructura de líneas del documento original, aunque puede introducir errores en layouts muy complejos (tablas anidadas, texto en columnas).

+

El texto de referencia se extrajo directamente del PDF mediante PyMuPDF, preservando la estructura de líneas del documento original. Esta aproximación puede introducir errores en layouts muy complejos (tablas anidadas, texto en columnas). La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).

Configuración de los Modelos

-

Según el código en ocr_benchmark_notebook.ipynb:

-

EasyOCR:

-

import easyocr
-
-easyocr_reader = easyocr.Reader(['es', 'en'])  # Spanish and English
-results = easyocr_reader.readtext(image_path)
-text = ' '.join([r[1] for r in results])

-

La configuración incluye soporte para español e inglés, permitiendo reconocer palabras en ambos idiomas que puedan aparecer en documentos académicos (referencias, términos técnicos).

-

PaddleOCR (PP-OCRv5):

-

from paddleocr import PaddleOCR
-
-paddleocr_model = PaddleOCR(
-    text_detection_model_name="PP-OCRv5_server_det",
-    text_recognition_model_name="PP-OCRv5_server_rec",
-    use_doc_orientation_classify=False,
-    use_doc_unwarping=False,
-    use_textline_orientation=True,
-)
-
-result = paddleocr_model.predict(image_path)
-text = '\n'.join([line['rec_texts'][0] for line in result[0]['rec_res']])

-

Se utilizaron los modelos "server" que ofrecen mayor precisión a costa de mayor tiempo de inferencia. La versión utilizada fue PaddleOCR 3.2.0.

-

DocTR:

-

from doctr.models import ocr_predictor
-
-doctr_model = ocr_predictor(
-    det_arch="db_resnet50",
-    reco_arch="sar_resnet31",
-    pretrained=True
-)
-
-result = doctr_model([image])
-text = result.render()

-

Se seleccionaron las arquitecturas db_resnet50 para detección y sar_resnet31 para reconocimiento, representando una configuración de alta precisión.

+

La configuración de cada modelo se detalla en src/ocr_benchmark_notebook.ipynb (ver Anexo A):

+

·     EasyOCR: Configurado con soporte para español e inglés, permitiendo reconocer palabras en ambos idiomas que puedan aparecer en documentos académicos (referencias, términos técnicos).

+

·     PaddleOCR (PP-OCRv5): Se utilizaron los modelos "server" (PP-OCRv5_server_det y PP-OCRv5_server_rec) que ofrecen mayor precisión a costa de mayor tiempo de inferencia. La versión utilizada fue PaddleOCR 3.2.0.

+

·     DocTR: Se seleccionaron las arquitecturas db_resnet50 para detección y sar_resnet31 para reconocimiento, representando una configuración de alta precisión.

Métricas de Evaluación

-

Se utilizó la biblioteca jiwer para calcular CER y WER de manera estandarizada:

-

from jiwer import wer, cer
-
-def evaluate_text(reference, prediction):
-    """
-    Calcula métricas de error entre texto de referencia y predicción.
-
-    Args:
-        reference: Texto ground truth
-        prediction: Texto predicho por el OCR
-
-    Returns:
-        dict con WER y CER
-    """
-    # Normalización básica
-    ref_clean = reference.lower().strip()
-    pred_clean = prediction.lower().strip()
-
-    return {
-        'WER': wer(ref_clean, pred_clean),
-        'CER': cer(ref_clean, pred_clean)
-    }

-

La normalización a minúsculas y eliminación de espacios extremos asegura una comparación justa que no penaliza diferencias de capitalización.

+

Se utilizó la biblioteca jiwer para calcular CER y WER de manera estandarizada. La normalización a minúsculas y eliminación de espacios extremos asegura una comparación justa que no penaliza diferencias de capitalización. La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).

Resultados del Benchmark

Resultados de PaddleOCR (Configuración Baseline)

Durante el benchmark inicial se evaluó PaddleOCR con configuración por defecto en un subconjunto del dataset. Los resultados preliminares mostraron variabilidad significativa entre páginas, con CER entre 1.54% y 6.40% dependiendo de la complejidad del layout.

@@ -5319,23 +5211,29 @@ def evaluate_text(reference, prediction):

Fuente: Elaboración propia.

 

Arquitectura de Ejecución

-

Debido a incompatibilidades entre Ray y PaddleOCR cuando se ejecutan en el mismo proceso, se implementó una arquitectura basada en subprocesos:

-

Figura 6. Arquitectura de ejecución con subprocesos

-

Arquitectura de ejecución con subprocesos

+

La arquitectura basada en contenedores Docker es fundamental para este proyecto debido a los conflictos de dependencias inherentes entre los diferentes componentes:

+

·     Conflictos entre motores OCR: PaddleOCR, DocTR y EasyOCR tienen dependencias mutuamente incompatibles (diferentes versiones de PyTorch/PaddlePaddle, OpenCV, etc.)

+

·     Incompatibilidades CUDA/cuDNN: Cada motor OCR requiere versiones específicas de CUDA y cuDNN que no pueden coexistir en un mismo entorno virtual

+

·     Aislamiento de Ray Tune: Ray Tune tiene sus propias dependencias que pueden entrar en conflicto con las librerías de inferencia OCR

+

Esta arquitectura containerizada permite ejecutar cada componente en su entorno aislado óptimo, comunicándose via API REST:

+

Figura 6. Arquitectura de ejecución con Docker Compose

+

Arquitectura de ejecución con Docker Compose

Fuente: Elaboración propia.

 

-

El script src/paddle_ocr_tuning.py actúa como wrapper que:

-

1.   Recibe hiperparámetros por línea de comandos

-

2.   Inicializa PaddleOCR con la configuración especificada

-

3.   Evalúa sobre el dataset

-

4.   Retorna métricas en formato JSON

-

python paddle_ocr_tuning.py \
-    --pdf-folder ./dataset \
-    --textline-orientation True \
-    --text-det-box-thresh 0.5 \
-    --text-det-thresh 0.4 \
-    --text-rec-score-thresh 0.6

-

Salida:

+

La arquitectura containerizada (src/docker-compose.tuning.*.yml) ofrece:

+

1.   Aislamiento de dependencias entre Ray Tune y los motores OCR

+

2.   Health checks automáticos para asegurar disponibilidad del servicio

+

3.   Comunicación via API REST (endpoints /health y /evaluate)

+

4.   Soporte para GPU mediante nvidia-docker

+

# Iniciar servicio OCR con GPU
+docker compose -f docker-compose.tuning.doctr.yml up -d doctr-gpu
+
+# Ejecutar optimización (64 trials)
+docker compose -f docker-compose.tuning.doctr.yml run raytune --service doctr --samples 64
+
+# Detener servicios
+docker compose -f docker-compose.tuning.doctr.yml down

+

Respuesta del servicio OCR:

{
     "CER": 0.0125,
     "WER": 0.1040,
@@ -5349,46 +5247,9 @@ def evaluate_text(reference, prediction):
 

Característica

Valor

Páginas totales

24

Páginas por trial

5 (páginas 5-10)

Estructura

Carpetas img/ y txt/ pareadas

Resolución

300 DPI

Formato imagen

PNG

Fuente: Elaboración propia.

 

-

La clase ImageTextDataset en src/dataset_manager.py gestiona la carga de pares imagen-texto:

-

class ImageTextDataset:
-    def __init__(self, root):
-        """
-        Carga pares (imagen, texto) de carpetas pareadas.
-
-        Estructura esperada:
-        root/
-          0/
-            img/
-              page_0001.png
-            txt/
-              page_0001.txt
-        """
-        self.pairs = []
-        for doc_folder in sorted(os.listdir(root)):
-            img_folder = os.path.join(root, doc_folder, 'img')
-            txt_folder = os.path.join(root, doc_folder, 'txt')
-            # Cargar pares...
-
-    def __getitem__(self, idx):
-        img_path, txt_path = self.pairs[idx]
-        return PIL.Image.open(img_path), open(txt_path).read()

+

La clase ImageTextDataset gestiona la carga de pares imagen-texto desde la estructura de carpetas pareadas. La implementación está disponible en el repositorio (ver Anexo A).

Espacio de Búsqueda

-

El espacio de búsqueda se definió considerando los hiperparámetros más relevantes identificados en la documentación de PaddleOCR:

-

from ray import tune
-from ray.tune.search.optuna import OptunaSearch
-
-search_space = {
-    # Parámetros booleanos
-    "use_doc_orientation_classify": tune.choice([True, False]),
-    "use_doc_unwarping": tune.choice([True, False]),
-    "textline_orientation": tune.choice([True, False]),
-
-    # Parámetros continuos (umbrales)
-    "text_det_thresh": tune.uniform(0.0, 0.7),
-    "text_det_box_thresh": tune.uniform(0.0, 0.7),
-    "text_det_unclip_ratio": tune.choice([0.0]),  # Fijado
-    "text_rec_score_thresh": tune.uniform(0.0, 0.7),
-}

+

El espacio de búsqueda se definió considerando los hiperparámetros más relevantes identificados en la documentación de PaddleOCR, utilizando tune.choice() para parámetros booleanos y tune.uniform() para umbrales continuos. La implementación está disponible en src/raytune/raytune_ocr.py (ver Anexo A).

Tabla 25. Descripción detallada del espacio de búsqueda.

Parámetro

Tipo

Rango

Descripción

use_doc_orientation_classify

Booleano

{True, False}

Clasificación de orientación del documento completo

use_doc_unwarping

Booleano

{True, False}

Corrección de deformación/curvatura

textline_orientation

Booleano

{True, False}

Clasificación de orientación por línea de texto

text_det_thresh

Continuo

[0.0, 0.7]

Umbral de probabilidad para píxeles de texto

text_det_box_thresh

Continuo

[0.0, 0.7]

Umbral de confianza para cajas detectadas

text_det_unclip_ratio

Fijo

0.0

Coeficiente de expansión (no explorado)

text_rec_score_thresh

Continuo

[0.0, 0.7]

Umbral de confianza de reconocimiento

Fuente: Elaboración propia.

@@ -5398,21 +5259,7 @@ search_space = {

1.   text_det_unclip_ratio fijo: Por decisión de diseño inicial, este parámetro se mantuvo constante para reducir la dimensionalidad del espacio de búsqueda.

1.   Parámetros booleanos completos: Los tres parámetros de preprocesamiento se exploran completamente para identificar cuáles son necesarios para documentos digitales.

Configuración de Ray Tune

-

tuner = tune.Tuner(
-    trainable_paddle_ocr,
-    tune_config=tune.TuneConfig(
-        metric="CER",
-        mode="min",
-        search_alg=OptunaSearch(),
-        num_samples=64,
-        max_concurrent_trials=2
-    ),
-    run_config=air.RunConfig(
-        verbose=2,
-        log_to_file=False
-    ),
-    param_space=search_space
-)

+

Se configuró Ray Tune con OptunaSearch como algoritmo de búsqueda, optimizando CER en 64 trials con 2 ejecuciones concurrentes. La implementación está disponible en src/raytune/raytune_ocr.py (ver Anexo A).

Tabla 26. Parámetros de configuración de Ray Tune.

Parámetro

Valor

Justificación

Métrica objetivo

CER

Métrica estándar para OCR

Modo

min

Minimizar tasa de error

Algoritmo

OptunaSearch (TPE)

Eficiente para espacios mixtos

Número de trials

64

Balance entre exploración y tiempo

Trials concurrentes

2

Limitado por memoria disponible

Fuente: Elaboración propia.

@@ -5503,27 +5350,7 @@ Configuración óptima:

Recomendación: Evitar text_det_thresh < 0.1 en cualquier configuración.

Comparación Baseline vs Optimizado

Evaluación sobre Dataset Completo

-

La configuración óptima identificada se evaluó sobre el dataset completo de 24 páginas, comparando con la configuración baseline:

-

Configuración Baseline:

-

baseline_config = {
-    "textline_orientation": False,  # Valor por defecto
-    "use_doc_orientation_classify": False,
-    "use_doc_unwarping": False,
-    "text_det_thresh": 0.3,  # Valor por defecto
-    "text_det_box_thresh": 0.6,  # Valor por defecto
-    "text_det_unclip_ratio": 1.5,  # Valor por defecto
-    "text_rec_score_thresh": 0.5,  # Valor por defecto
-}

-

Configuración Optimizada:

-

optimized_config = {
-    "textline_orientation": True,
-    "use_doc_orientation_classify": False,
-    "use_doc_unwarping": False,
-    "text_det_thresh": 0.4690,
-    "text_det_box_thresh": 0.5412,
-    "text_det_unclip_ratio": 0.0,
-    "text_rec_score_thresh": 0.6350,
-}

+

La configuración óptima identificada se evaluó sobre el dataset completo de 24 páginas, comparando con la configuración baseline (valores por defecto de PaddleOCR). Los parámetros optimizados más relevantes fueron: textline_orientation=True, text_det_thresh=0.4690, text_det_box_thresh=0.5412, y text_rec_score_thresh=0.6350.

Tabla 35. Comparación baseline vs optimizado (24 páginas).

Modelo

CER

Precisión Caracteres

WER

Precisión Palabras

PaddleOCR (Baseline)

7.78%

92.22%

14.94%

85.06%

PaddleOCR-HyperAdjust

1.49%

98.51%

7.62%

92.38%

Fuente: Elaboración propia.

@@ -5557,7 +5384,7 @@ Configuración óptima:

3.   Con GPU, los tiempos serían 10-50× menores según benchmarks de PaddleOCR.

Resumen de la Sección

Esta sección ha presentado:

-

1.   Configuración del experimento: Arquitectura de subprocesos, dataset extendido, espacio de búsqueda de 7 dimensiones

+

1.   Configuración del experimento: Arquitectura Docker Compose, dataset extendido, espacio de búsqueda de 7 dimensiones

1.   Resultados estadísticos:

- CER medio: 5.25% (std: 11.03%) - CER mínimo: 1.15% - 67.2% de trials con CER < 2%

1.   Hallazgos clave:

@@ -5647,22 +5474,10 @@ Configuración óptima:

Implicaciones Prácticas

Guía de Configuración Recomendada

Para documentos académicos en español similares a los evaluados:

-

Configuración recomendada:

-

config_recomendada = {
-    # OBLIGATORIO
-    "textline_orientation": True,
-
-    # RECOMENDADO
-    "text_det_thresh": 0.45,  # Rango: 0.4-0.5
-    "text_rec_score_thresh": 0.6,  # Rango: 0.5-0.7
-
-    # OPCIONAL
-    "text_det_box_thresh": 0.55,  # Rango: 0.5-0.6
-
-    # NO RECOMENDADO para PDFs digitales
-    "use_doc_orientation_classify": False,
-    "use_doc_unwarping": False,
-}

+

Tabla 46. Configuración recomendada para PaddleOCR.

+

Parámetro

Valor

Prioridad

Justificación

textline_orientation

True

Obligatorio

Reduce CER en 69.7%

text_det_thresh

0.45 (rango: 0.4-0.5)

Recomendado

Correlación fuerte con CER

text_rec_score_thresh

0.6 (rango: 0.5-0.7)

Recomendado

Filtra reconocimientos poco confiables

text_det_box_thresh

0.55 (rango: 0.5-0.6)

Opcional

Impacto moderado

use_doc_orientation_classify

False

No recomendado

Innecesario para PDFs digitales

use_doc_unwarping

False

No recomendado

Innecesario para PDFs digitales

+

Fuente: Elaboración propia.

+

 

Cuándo Aplicar Esta Metodología

La optimización de hiperparámetros es recomendable cuando:

1.   Sin GPU disponible: El fine-tuning requiere GPU; la optimización de hiperparámetros no.

@@ -5699,21 +5514,21 @@ Configuración óptima:

Validación con Aceleración GPU

Para evaluar la viabilidad práctica del enfoque optimizado en escenarios de producción, se realizó una validación adicional utilizando aceleración GPU. Esta fase complementa los experimentos en CPU presentados anteriormente y demuestra la aplicabilidad del método cuando se dispone de hardware con capacidad de procesamiento paralelo.

Configuración del Entorno GPU

-

Tabla 46. Especificaciones del entorno de validación GPU.

+

Tabla 47. Especificaciones del entorno de validación GPU.

Componente

Especificación

GPU

NVIDIA GeForce RTX 3060 Laptop

VRAM

5.66 GB

CUDA

12.4

Sistema Operativo

Ubuntu 24.04.3 LTS

Kernel

6.14.0-37-generic

Fuente: Elaboración propia.

 

El entorno de validación representa hardware de consumo típico para desarrollo de aplicaciones de machine learning, permitiendo evaluar el rendimiento en condiciones realistas de despliegue.

Comparación CPU vs GPU

Se evaluó el tiempo de procesamiento utilizando la configuración optimizada identificada en la fase anterior, comparando el rendimiento entre CPU y GPU.

-

Tabla 47. Rendimiento comparativo CPU vs GPU.

+

Tabla 48. Rendimiento comparativo CPU vs GPU.

Métrica

CPU

GPU (RTX 3060)

Factor de Aceleración

Tiempo/Página

69.4s

0.55s

126x

Dataset completo (45 páginas)

~52 min

~25 seg

126x

Fuente: Elaboración propia.

 

La aceleración de 126x obtenida con GPU transforma la aplicabilidad práctica del sistema. Mientras que el procesamiento en CPU limita el uso a escenarios de procesamiento por lotes sin restricciones de tiempo, la velocidad con GPU habilita casos de uso interactivos y de tiempo real.

Comparación de Modelos PaddleOCR

PaddleOCR ofrece dos variantes de modelos: Mobile (optimizados para dispositivos con recursos limitados) y Server (mayor precisión a costa de mayor consumo de memoria). Se evaluó la viabilidad de ambas variantes en el hardware disponible.

-

Tabla 48. Comparación de modelos Mobile vs Server en RTX 3060.

+

Tabla 49. Comparación de modelos Mobile vs Server en RTX 3060.

Modelo

VRAM Requerida

Resultado

Recomendación

PP-OCRv5 Mobile

0.06 GB

Funciona correctamente

✓ Recomendado

PP-OCRv5 Server

5.3 GB

OOM en página 2

✗ Requiere >8 GB VRAM

Fuente: Elaboración propia.

 

@@ -5732,7 +5547,7 @@ y trabajo futuro

Este capít

Conclusiones Generales

Este Trabajo Fin de Máster ha demostrado que es posible mejorar significativamente el rendimiento de sistemas OCR preentrenados mediante optimización sistemática de hiperparámetros, sin requerir fine-tuning ni recursos GPU dedicados.

El objetivo principal del trabajo era alcanzar un CER inferior al 2% en documentos académicos en español. Los resultados obtenidos confirman el cumplimiento de este objetivo:

-

Tabla 49. Cumplimiento del objetivo de CER.

+

Tabla 50. Cumplimiento del objetivo de CER.

Métrica

Objetivo

Resultado

CER

< 2%

1.49%

Fuente: Elaboración propia.

 

@@ -5752,7 +5567,7 @@ y trabajo futuro

Este capít

Respecto a OE4 (Optimización con Ray Tune):

·     Se ejecutaron 64 trials con el algoritmo OptunaSearch

·     El tiempo total del experimento fue aproximadamente 6 horas (en CPU)

-

·     La arquitectura basada en subprocesos permitió superar incompatibilidades entre Ray y PaddleOCR

+

·     La arquitectura basada en contenedores Docker permitió superar incompatibilidades entre Ray y los motores OCR, facilitando además la portabilidad y reproducibilidad

Respecto a OE5 (Validación de la configuración):

·     Se validó la configuración óptima sobre el dataset completo de 24 páginas

·     La mejora obtenida fue del 80.9% en reducción del CER (7.78% → 1.49%)

@@ -5854,12 +5669,12 @@ major-latin;mso-bidi-font-family:"Calibri Light";mso-bidi-theme-font:major-latin └── .gitea/workflows/ci.yaml # Pipeline CI/CD

A.3 Requisitos de Software

Sistema de Desarrollo

-

Tabla 50. Especificaciones del sistema de desarrollo.

+

Tabla 51. Especificaciones del sistema de desarrollo.

Componente

Especificación

Sistema Operativo

Ubuntu 24.04.3 LTS

CPU

AMD Ryzen 7 5800H

RAM

16 GB DDR4

GPU

NVIDIA RTX 3060 Laptop (5.66 GB VRAM)

CUDA

12.4

Fuente: Elaboración propia.

 

Dependencias

-

Tabla 51. Dependencias del proyecto.

+

Tabla 52. Dependencias del proyecto.

Componente

Versión

Python

3.12.3

Docker

29.1.5

NVIDIA Container Toolkit

Requerido para GPU

Ray

2.52.1

Optuna

4.7.0

Fuente: Elaboración propia.

 

@@ -5935,7 +5750,7 @@ results = run_tuner(trainable, PADDLE_OCR_SEARCH_SPACE, num_samples=64) analyze_results(results, prefix='raytune_paddle', config_keys=PADDLE_OCR_CONFIG_KEYS) "

Servicios y Puertos

-

Tabla 52. Servicios Docker y puertos.

+

Tabla 53. Servicios Docker y puertos.

Servicio

Puerto

Script de Ajuste

PaddleOCR

8002

paddle_ocr_payload

DocTR

8003

doctr_payload

EasyOCR

8002

easyocr_payload

Fuente: Elaboración propia.

 

@@ -5946,7 +5761,7 @@ analyze_results(results, prefix='raytune_paddle', config_keys=PADDLE_OCR_CONFIG_

·     DocTR - Más rápido (0.50s/página)

·     EasyOCR - Balance intermedio

Resumen de Resultados

-

Tabla 53. Resumen de resultados del benchmark por servicio.

+

Tabla 54. Resumen de resultados del benchmark por servicio.

Servicio

CER Base

CER Ajustado

Mejora

PaddleOCR

8.85%

7.72%

12.8%

DocTR

12.06%

12.07%

0%

EasyOCR

11.23%

11.14%

0.8%

Fuente: Elaboración propia.