regen docs
Some checks failed
build_docker / build_cpu (pull_request) Has been cancelled
build_docker / build_gpu (pull_request) Has been cancelled
build_docker / build_easyocr (pull_request) Has been cancelled
build_docker / build_easyocr_gpu (pull_request) Has been cancelled
build_docker / build_raytune (pull_request) Has been cancelled
build_docker / build_doctr (pull_request) Has been cancelled
build_docker / essential (pull_request) Has been cancelled
build_docker / build_doctr_gpu (pull_request) Has been cancelled

This commit is contained in:
2026-01-19 17:38:43 +01:00
parent b1539fd79f
commit 07a7ba8c01
4 changed files with 138 additions and 541 deletions

View File

@@ -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