5e6023f639
Cierra el ciclo del analysis gliner_glirel_tuning: documenta en app.md el pipeline NER+RE disponible en el registry y abre los dos issues que faltan para cablearlo en extract_graph_hybrid + panel paste_extract. Archiva el 0042 original (mREBEL) tras la decision a favor de GLiNER2 (Apache 2.0, joint NER+RE, 20-30x mas rapido en CPU). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
192 lines
7.2 KiB
Markdown
192 lines
7.2 KiB
Markdown
---
|
||
id: 0042
|
||
title: GLiNER2 — extractor unificado NER+RE (Apache 2.0) sustituye GLiREL en extract_graph_hybrid
|
||
status: pending
|
||
priority: high
|
||
created: 2026-05-04
|
||
parent: 0013
|
||
depends_on: [0041]
|
||
supersedes: 0042-mrebel-relation-extractor (archivado, ver decisions/2026-05-04-gliner2-over-mrebel.md)
|
||
---
|
||
|
||
## Contexto
|
||
|
||
El issue 0042 inicial (sustituir GLiREL por mREBEL) queda superado por el descubrimiento de **GLiNER2** (`fastino/gliner2-large-v1`):
|
||
|
||
- **Apache 2.0** — sin restriccion comercial.
|
||
- **NER + RE en un solo modelo** — un solo forward pass.
|
||
- **20-30× mas rapido** que mREBEL en CPU.
|
||
- **Funciona en OSINT castellano** — captura IPs, dominios defanged, CVEs, malware.
|
||
|
||
Datos en `vaults/osint_nlp_models/models/gliner2.md`, decision en `vaults/osint_nlp_models/decisions/2026-05-04-gliner2-over-mrebel.md`, validacion empirica en `analysis/gliner_glirel_tuning/notebooks/04_gliner2_winner.ipynb`.
|
||
|
||
## Objetivo
|
||
|
||
Anadir GLiNER2 como motor principal de NER+RE en el pipeline `extract_graph_hybrid`. GLiREL queda como engine secundario en el panel `paste_extract`. mREBEL queda disponible via las funciones del registry pero no por defecto.
|
||
|
||
## Diseño
|
||
|
||
### 1. Funciones nuevas en el registry
|
||
|
||
```
|
||
python/functions/datascience/
|
||
├── gliner2_load_model.py # carga + cache GLiNER2 (Apache 2.0)
|
||
└── extract_graph_gliner2.py # ejecuta schema entity+relation, normaliza a EntityCandidate/RelationCandidate
|
||
```
|
||
|
||
Plantilla `gliner2_load_model.py`:
|
||
|
||
```python
|
||
"""Carga (y cachea) un modelo GLiNER2 que hace NER+RE conjunto.
|
||
|
||
LICENCIA: Apache 2.0 — uso libre incluido comercial.
|
||
"""
|
||
from typing import Any
|
||
_MODEL_CACHE: dict[tuple[str, str], Any] = {}
|
||
|
||
def gliner2_load_model(
|
||
model_name: str = "fastino/gliner2-large-v1",
|
||
device: str = "auto",
|
||
) -> Any:
|
||
"""Returns GLiNER2 instance ready for .extract() with schemas."""
|
||
resolved = _resolve_device(device)
|
||
key = (model_name, resolved)
|
||
if key in _MODEL_CACHE: return _MODEL_CACHE[key]
|
||
from gliner2 import GLiNER2
|
||
m = GLiNER2.from_pretrained(model_name)
|
||
if hasattr(m, "to"): m.to(resolved)
|
||
_MODEL_CACHE[key] = m
|
||
return m
|
||
```
|
||
|
||
Plantilla `extract_graph_gliner2.py`:
|
||
|
||
```python
|
||
def extract_graph_gliner2(
|
||
text: str,
|
||
entity_labels: list[str],
|
||
relation_labels: list[str],
|
||
model,
|
||
chunk_by_sentences: bool = False,
|
||
) -> tuple[list, list]:
|
||
"""Returns (entities, relations) as list[EntityCandidate], list[RelationCandidate].
|
||
|
||
If chunk_by_sentences=True, splits text by sentence and aggregates.
|
||
Recommended for texts > 20 sentences (recall improves).
|
||
"""
|
||
```
|
||
|
||
### 2. Cambio en `extract_graph_hybrid.py`
|
||
|
||
Anadir parametro `relation_engine` con cuatro valores:
|
||
|
||
```python
|
||
def extract_graph_hybrid(
|
||
chunks, entity_schema, relation_types,
|
||
relation_engine: str = "gliner2", # 'gliner2' | 'glirel' | 'mrebel' | 'none'
|
||
gliner_model=None, # capa 1 NER (legacy stack)
|
||
glirel_model=None, # capa 4 RE (legacy)
|
||
mrebel_models=None, # tuple (tok, mdl) si engine=mrebel
|
||
gliner2_model=None, # NUEVO — sustituye gliner+glirel
|
||
llm_chat_json=None,
|
||
entity_threshold=0.5,
|
||
relation_threshold=0.15,
|
||
chunk_by_sentences=False,
|
||
):
|
||
if relation_engine == "gliner2":
|
||
ents, rels = extract_graph_gliner2(text=..., entity_labels=..., relation_labels=...,
|
||
model=gliner2_model, chunk_by_sentences=chunk_by_sentences)
|
||
# NO se llama a GLiNER ni GLiREL, GLiNER2 hace ambos.
|
||
elif relation_engine == "glirel":
|
||
# legacy: GLiNER + GLiREL como antes
|
||
...
|
||
elif relation_engine == "mrebel":
|
||
# GLiNER + mREBEL via las funciones del registry (extract_relations_mrebel)
|
||
...
|
||
elif relation_engine == "none":
|
||
rels = []
|
||
```
|
||
|
||
### 3. `enrichers/paste_extract/run.py`
|
||
|
||
Manifest:
|
||
|
||
```yaml
|
||
params:
|
||
text: { type: string, required: true }
|
||
use_hybrid: { type: bool, default: true }
|
||
relation_engine: { type: enum, values: [gliner2, glirel, mrebel, none], default: gliner2 }
|
||
entity_threshold: { type: float, default: 0.5 }
|
||
relation_threshold: { type: float, default: 0.15 }
|
||
chunk_by_sentences: { type: bool, default: false }
|
||
```
|
||
|
||
### 4. Panel C++ (`extract_panel.cpp`)
|
||
|
||
- Combo: `Engine: [GLiNER2 (recomendado) | GLiNER+GLiREL (rapido EN) | GLiNER+mREBEL (no comercial) | None]`
|
||
- Banner amarillo solo si `mREBEL` seleccionado (recordatorio de licencia CC BY-NC-SA).
|
||
- Checkbox `chunk_by_sentences` (recomendado para texto largo).
|
||
- Sliders del issue 0041 (entity_threshold, relation_threshold).
|
||
|
||
### 5. `app.md`
|
||
|
||
```yaml
|
||
notes: |
|
||
Motor de extraccion por defecto: GLiNER2 (Apache 2.0, NER+RE conjunto, ES/EN/FR).
|
||
Engines alternativos: GLiNER+GLiREL (rapido EN), GLiNER+mREBEL (no comercial,
|
||
18 idiomas). Ver vaults/osint_nlp_models/ para benchmarks.
|
||
|
||
dependencies:
|
||
- "gliner2 >= 1.3.0 (Apache 2.0)"
|
||
optional_dependencies:
|
||
- "gliner + glirel (Apache 2.0, legacy)"
|
||
- "transformers + sentencepiece (mREBEL/REBEL — non-commercial mREBEL)"
|
||
```
|
||
|
||
### 6. Tests pytest
|
||
|
||
`tests/test_extract_graph_gliner2.py`:
|
||
|
||
- Test smoke con stub de modelo (mock que retorna shape oficial).
|
||
- Test que normaliza output a `EntityCandidate` / `RelationCandidate` correctamente.
|
||
- Test que `chunk_by_sentences=True` agrega correctamente.
|
||
|
||
`tests/test_paste_extract.py`: anadir test del path `relation_engine=gliner2` (default).
|
||
|
||
## Definicion de hecho
|
||
|
||
- Pego el corpus es_corporate_short, dejo engine=GLiNER2, pulso Extract.
|
||
- Tras ~1.5s veo 14 entidades y 8 relaciones, mayoria correctas.
|
||
- Cambio engine a GLiREL → resultado pobre conocido (validacion contraria).
|
||
- Cambio engine a mREBEL → veo banner de licencia + 5 relaciones lentas pero limpias.
|
||
- Pego corpus es_osint con label set OSINT → veo IPs, CVEs, malware, hashes correctamente.
|
||
- Apply solo guarda las marcadas; dedupe sigue funcionando.
|
||
- Tests verdes en WSL.
|
||
|
||
## Latencia / UX
|
||
|
||
- GLiNER2: ~1-5s para texto medio (4-30 frases). UX **fluida**, no necesita progress bar.
|
||
- GLiNER2 chunked: ~150ms/frase. Para 30 frases = 4.5s, sigue fluido.
|
||
- mREBEL queda con barra de progreso obligatoria (issue separado dentro de este).
|
||
|
||
## Out of scope (issues separados)
|
||
|
||
- Threshold tuning interno de GLiNER2 (si la API lo expone).
|
||
- Mapeo de tipos de entidad GLiNER2 a vocabulario controlado custom (ej. `persona` → `Person`).
|
||
- Extension a Frances (declarado pero sin probar).
|
||
- Comparativa con `gliner2-base-v1` (mas pequeño) — issue de optimizacion.
|
||
|
||
## Riesgos
|
||
|
||
- **Recall bajo en texto largo** sin chunking. Mitigacion: `chunk_by_sentences=True` por defecto si texto > 20 frases.
|
||
- **Errores semanticos puntuales** (e.g. `Inditex acquired Pablo Isla`). El panel los muestra con checkbox — el humano filtra antes de Apply.
|
||
- **Spans sucios ocasionales** en OSINT. Mitigacion: aplicar `align_relations_to_entities_py_datascience` (funcion ya en el registry) post-extract para descartar relaciones cuyas puntas no son entidades reales.
|
||
|
||
## Relacionado
|
||
|
||
- `vaults/osint_nlp_models/models/gliner2.md`
|
||
- `vaults/osint_nlp_models/decisions/2026-05-04-gliner2-over-mrebel.md`
|
||
- `analysis/gliner_glirel_tuning/notebooks/04_gliner2_winner.ipynb`
|
||
- Issue 0041 (split confidence_threshold) — sigue valido.
|
||
- Issue 0042-mrebel-relation-extractor.md.superseded (archivado).
|