Hablemos de su proyecto
Inteligencia artificial

Alinear dos planes de clasificación con transporte óptimo no equilibrado

Durante una migración GED, algunas categorías cambian de nombre, se dividen, se fusionan o no tienen equivalente. Una matriz explicable propone enlaces múltiples con costes, masas y advertencias.

Parcours pédagogique : comprendre le cas, construire, mesurer puis vérifier.

Durante una migración GED, algunas categorías cambian de nombre, se dividen, se fusionan o no tienen equivalente. Un emparejamiento uno a uno forzado crea correspondencias engañosas y desplaza a veces documentos a una categoría inadecuada.

Una matriz explicable propone enlaces múltiples con costes, masas y advertencias, y exporta un archivo de revisión. No se crea automáticamente ningún skos:exactMatch ni desplazamiento de documento.

Descargar el proyecto completo, revisión r001

El recorrido del tutorial, desde el caso concreto hasta la verificación.

Un corpus de correspondencias múltiples

Veinticuatro pares de planes tienen cada uno 8 conceptos fuente y 10 objetivo, es decir, 192 y 240 conceptos. Doce dominios sirven para calibración y doce para test. Dominios y envolturas textuales son distintos, pero prototipos semánticos de relación y sinónimos se comparten: no es una experiencia totalmente fuera de distribución. Esto hace mecanismos legibles; rendimiento no prueba generalización de negocio.

Cada par tiene 64 notas originales de carga. Sus dos asignaciones de clasificación se simulan independientemente y producen a y b por recuentos/64; no son anotaciones de correspondencia. Todos los conceptos principales tienen masa positiva. Gold contiene relaciones uno-a-varios, varios-a-uno y una categoría sin equivalente. Permanece en archivo separado y nunca entra en costes.

Los broader permanecen internos a un plan; el código rechaza duplicados, padres ausentes y ciclos. Este TP adopta convención acíclica local, no define todas las restricciones generales SKOS. Ningún export declara skos:exactMatch ni se aplica migración.

E5: sentido y contexto

E5-small congelado en 614241f622f53c4eeff9890bdc4f31cfecc418b3 codifica etiqueta+definición y contexto de ancestros. Prefijo query: es simétrico para fuente y destino. Pooling enmascarado, tokens especiales incluidos y normalización L2 en 384 dimensiones; código rechaza más de 512 tokens y verifica archivos y tensores.

Una raíz usa misma frase explícita de contexto, por tanto raíz/raíz tiene contexto idéntico. Coste Cij=1−cos(ei,ej)+0,25(1−cos(ai,aj)). Factor 0,25 fijo. Cosenos FP32 se acumulan FP64 y se limitan a [-1,1] por redondeo. Costes no conocen gold.

Tres métodos, tres interpretaciones

Top 1 coseno toma destino textualmente más cercano con umbral de calibración. Ignora masas y no cambia en ejercicio de masas. Transporte equilibrado impone márgenes a y b; puede mover carga a destino semánticamente pobre para satisfacerlos.

El transporte no equilibrado minimiza suma(ΠC)+epsilon suma(Π(logΠ−1))+rho_a KL(Π1||a)+rho_b KL(Πᵀ1||b). KL generalizada es suma(p log(p/q)−p+q), con cero explícito. Penalizaciones permiten déficit y exceso; margen puede superar carga inicial. Ninguna cantidad es probabilidad de correspondencia.

El núcleo logarítmico es −C/epsilon. Se alternan logu y logv mediante logsumexp estabilizado. Rho infinito da caso equilibrado. Masas nulas se retiran y reintegran como filas/columnas nulas. Totales nulos, datos no finitos, parámetros inválidos o falta de convergencia se rechazan.

Convergencia exige variación máxima de potenciales logarítmicos de 1e-11 bajo 20000 iteraciones y márgenes equilibrados de 1e-8. Caso 2×2 se compara con POT 0.9.7 FP64 con reg_type='entropy' explícito y tolerancia de plan 1e-8. Ambos planes usan nuestra fórmula de objetivo; total_cost POT no se asimila ciegamente.

Calibrar antes del test

Epsilon recorre 0,03 y 0,1; rho 0,1, 1 y 10. Umbrales de enlace usan Πij/ai y revisión suma_jΠij/ai. Estos ratios pueden superar 1 y no son probabilidades ni confianza. business_scope excluye propuestas financieras incompatibles; solver usa costes finitos y regla no es restricción oculta.

Grillas y orden quedan fijados. En 12 pares de calibración cada método elige micro-F1 de enlaces múltiples y primer elemento en empate. Los 12 pares test no eligen umbral, epsilon ni rho. Planes test se calculan sin gold; métricas solo tras calibración.

Propuestas se comparan con gold: TP/FP/FN, precisión, recall, F1, cobertura fuente, enlaces falsos en categoría sin equivalente, REVIEW y EXCLUDED. Salidas indican márgenes, déficits, excesos, objetivo y residuo solver. Tiempo CPU solver se separa de encoding GPU. Mejor puntuación no justifica migración automática.

Ejercicio con embeddings congelados

El par test 12 se elige antes de resultados. Multiplique masa de Visitas taller por diez y renormalice a, sin cambiar b, embeddings ni parámetros. Recalcule equilibrado y no equilibrado. Top 1 permanece textual; planes pueden cambiar. Lea valores sin exigir ganancia y compare definiciones antes de decisión humana. Salidas principales quedan protegidas por hash.

Paso 01 — Cargar planes como conceptos identificados

SKOS, etiquetas y jerarquía

Error: Los conceptos se fusionan solo por etiqueta

Corrección: Conservar URI y plan de origen; rechazar ciclos en el mínimo.

Archivo src/ged_migration_taxonomies/data.py

import hashlib, json, random
from .config import save_json

DOMAINS = [
    "water pumping",
    "railway switches",
    "solar panels",
    "industrial cooling",
    "harbour cranes",
    "food conveyors",
    "hospital ventilation",
    "urban elevators",
    "hydraulic presses",
    "optical sensors",
    "textile looms",
    "district heating",
    "wind turbines",
    "laboratory freezers",
    "mining drills",
    "airport lighting",
    "marine generators",
    "paper rollers",
    "waste sorting",
    "telecom antennas",
    "irrigation gates",
    "battery storage",
    "ceramic furnaces",
    "robotic welders",
]
SOURCE = [
    (
        "Visites atelier",
        "Scheduled routine and exceptional site visits for equipment maintenance.",
    ),
    ("Inspections", "Safety inspections and conformity observations."),
    (
        "Étalonnage",
        "Calibration of measurement instruments against reference standards.",
    ),
    ("Remises en état", "Repair of defective components and restoration of service."),
    ("Commandes neuves", "Purchase orders for new equipment from external suppliers."),
    ("Renouvellements", "Replacement equipment procurement from external suppliers."),
    (
        "Ancien registre papier",
        "Retired paper visitor sign-in book; no maintenance content.",
    ),
    (
        "Comptes rendus",
        "Technical reports covering completed jobs and incident investigations.",
    ),
]
TARGET = [
    ("Tournées programmées", "Routine scheduled equipment maintenance visits."),
    ("Déplacements urgents", "Exceptional unscheduled equipment maintenance visits."),
    ("Contrôles sécurité", "Safety inspection records and conformity observations."),
    ("Réglage métrologique", "Instrument calibration against reference standards."),
    ("Réparations", "Restoration of defective components to working service."),
    (
        "Approvisionnements",
        "Purchase and replacement equipment procurement from suppliers.",
    ),
    ("Rapports de travaux", "Technical reports about completed maintenance work."),
    ("Analyses incidents", "Technical investigation reports concerning incidents."),
    ("Archives financières", "Historical financial ledger and payment records."),
    ("Innovation", "Proposals for new engineering research projects."),
]
LINKS = [[0, 1], [2], [3], [4], [5], [5], [], [6, 7]]


def validate_broader(concepts):
    ids = {c["id"] for c in concepts}
    if len(ids) != len(concepts):
        raise ValueError("Duplicate concept ID")
    byid = {c["id"]: c for c in concepts}
    for c in concepts:
        seen = {c["id"]}
        parent = c["broader"]
        while parent is not None:
            if parent not in byid or parent in seen:
                raise ValueError("Invalid or cyclic broader relation")
            seen.add(parent)
            parent = byid[parent]["broader"]


def ancestors(concept, concepts):
    validate_broader(concepts)
    byid = {c["id"]: c for c in concepts}
    values = []
    parent = concept["broader"]
    while parent is not None:
        c = byid[parent]
        values.append(c["label"] + ". " + c["definition"])
        parent = c["broader"]
    return (
        " Ancestor: ".join(values)
        if values
        else "Root concept of this independent classification scheme."
    )


def prepare(config):
    pairs = []
    gold = []
    pieces = []
    templates = {}
    for family, domain in enumerate(DOMAINS):
        split = "calibration" if family < 12 else "test"
        template = (
            f"Independent {domain} vocabulary revision {family+1}."
            if split == "calibration"
            else f"The {domain} documentation office defines scope {family+1} below."
        )
        templates[str(family)] = {
            "id": f"template-{family:02d}",
            "split": split,
            "text": template,
        }
        source = []
        target = []
        for side, specs, parents in [
            ("s", SOURCE, [None, 0, 0, 0, None, 4, None, 0]),
            ("t", TARGET, [None, 0, 0, 2, 0, None, 0, 6, None, None]),
        ]:
            values = []
            for i, (label, definition) in enumerate(specs):
                values.append(
                    {
                        "id": f"P{family:02d}-{side}{i}",
                        "label": label,
                        "definition": f"{template} {definition} Domain: {domain}.",
                        "broader": (
                            None
                            if parents[i] is None
                            else f"P{family:02d}-{side}{parents[i]}"
                        ),
                        "business_scope": (
                            "financial" if side == "t" and i == 8 else "technical"
                        ),
                        "active": True,
                    }
                )
            validate_broader(values)
            if side == "s":
                source = values
            else:
                target = values
        rng = random.Random(17000 + family)
        src = list(range(8)) + [rng.randrange(8) for _ in range(56)]
        tgt = list(range(10)) + [rng.randrange(10) for _ in range(54)]
        rng.shuffle(src)
        rng.shuffle(tgt)
        for j, (a, b) in enumerate(zip(src, tgt)):
            body = f"Inventory note {family:02d}-{j:02d} about {domain}. Independently recorded source and destination filing loads, not an alignment annotation."
            pieces.append(
                {
                    "piece_id": f"piece-{family:02d}-{j:02d}",
                    "pair": family,
                    "text": body,
                    "text_sha256": hashlib.sha256(body.encode()).hexdigest(),
                    "source_bucket": source[a]["id"],
                    "target_bucket": target[b]["id"],
                }
            )
        a = [src.count(i) / 64 for i in range(8)]
        b = [tgt.count(i) / 64 for i in range(10)]
        pair = {
            "pair": family,
            "split": split,
            "domain": domain,
            "template_id": f"template-{family:02d}",
            "source": source,
            "target": target,
            "a": a,
            "b": b,
            "mass_source": "64 independently simulated filing-load notes; not gold link counts",
        }
        pairs.append(pair)
        gold.append(
            {
                "pair": family,
                "split": split,
                "links": [
                    [source[i]["id"], target[j]["id"]]
                    for i, links in enumerate(LINKS)
                    for j in links
                ],
                "without_equivalent": [source[6]["id"]],
            }
        )
    save_json("data/generated/input/pages.json", pairs)
    save_json("data/generated/input/pieces.json", pieces)
    save_json("data/generated/input/templates.json", templates)
    save_json("data/generated/labels/gold.json", gold)
    save_json(
        "data/generated/labels/validation.json",
        [g for g in gold if g["split"] == "calibration"],
    )
    save_json(
        "data/generated/manifest.json",
        {
            "pairs": 24,
            "source_concepts": 192,
            "target_concepts": 240,
            "pieces": 1536,
            "calibration_pairs": 12,
            "test_pairs": 12,
            "model_features_have_gold": False,
            "shared_semantic_relation_pattern": True,
            "domain_and_template_disjoint": True,
            "limits": "Concept relation pattern and synonym prototypes shared across pairs; distinct domains/templates do not make the task fully out-of-distribution.",
        },
    )

Paso 02 — Construir una primera correspondencia textual

Embeddings y coste simétrico

Error: Las correspondencias gold se utilizan para el contexto de ancestros

Corrección: Construir el contexto únicamente a partir de broader, sin acceder al mapeo gold.

Archivo src/ged_migration_taxonomies/features.py

import numpy as np
from .encoder import Encoder
from .data import ancestors
from .config import read_json, save_json


def baseline(config):
    pairs = read_json("data/generated/input/pages.json")
    encoder = Encoder(config)
    rows = []
    for pair in pairs:
        concepts = pair["source"] + pair["target"]
        texts = [c["label"] + ". " + c["definition"] for c in concepts]
        contexts = [
            ancestors(c, pair["source"] if i < 8 else pair["target"])
            for i, c in enumerate(concepts)
        ]
        values = encoder.encode_queries(texts).astype(np.float64)
        context = encoder.encode_queries(contexts).astype(np.float64)
        cosine = np.clip(values[:8] @ values[8:].T, -1, 1)
        ancestor_cosine = np.clip(context[:8] @ context[8:].T, -1, 1)
        cost = 1 - cosine + config["ancestor_weight"] * (1 - ancestor_cosine)
        rows.append(
            {
                "pair": pair["pair"],
                "concept_ids": [c["id"] for c in concepts],
                "texts": texts,
                "contexts": contexts,
                "embeddings": values.tolist(),
                "ancestor_embeddings": context.tolist(),
                "cosine": cosine.tolist(),
                "ancestor_cosine": ancestor_cosine.tolist(),
                "cost": cost.tolist(),
                "top1": [int(np.argmax(row)) for row in cosine],
            }
        )
    save_json(
        "outputs/features.json",
        {
            "rows": rows,
            "encoder": encoder.identity,
            "encoding_calls": encoder.calls,
            "labels_visible": False,
            "ancestor_weight": config["ancestor_weight"],
        },
    )

Paso 03 — Deducir los factores de escala del transporte

Entropía, KL de los marginales y espacio logarítmico

Error: Comparar reg_type='kl' con el cálculo entrópico del ejercicio

Corrección: Utilizar reg_type='entropy' en la referencia y explicar esta diferencia de convención.

Archivo src/ged_migration_taxonomies/transport.py

"""Entropy-regularized transport in float64 log space; explicit mass semantics."""

import math, time, warnings
import numpy as np


def logsumexp(values, axis):
    maximum = np.max(values, axis=axis, keepdims=True)
    return (
        maximum + np.log(np.exp(values - maximum).sum(axis=axis, keepdims=True))
    ).squeeze(axis)


def generalized_kl(p, q):
    p = np.asarray(p, dtype=np.float64)
    q = np.asarray(q, dtype=np.float64)
    if (
        np.any(p < 0)
        or np.any(q < 0)
        or not np.isfinite(p).all()
        or not np.isfinite(q).all()
    ):
        raise ValueError("Invalid KL masses")
    if np.any((q == 0) & (p > 0)):
        return math.inf
    active = p > 0
    return float(
        np.sum(p[active] * np.log(p[active] / q[active])) - np.sum(p) + np.sum(q)
    )


def objective(plan, cost, a, b, epsilon, rho_a, rho_b):
    positive = plan > 0
    entropy = float(np.sum(plan[positive] * (np.log(plan[positive]) - 1)))
    linear = float(np.sum(plan * cost))
    left = generalized_kl(plan.sum(1), a)
    right = generalized_kl(plan.sum(0), b)
    value = linear + epsilon * entropy
    if math.isfinite(rho_a):
        value += rho_a * left
    if math.isfinite(rho_b):
        value += rho_b * right
    return {
        "linear": linear,
        "negative_entropy": entropy,
        "row_KL": left,
        "column_KL": right,
        "regularized": value,
        "convention": "P*(logP-1); generalized KL includes -p+q",
    }


def sinkhorn_unbalanced(
    a, b, cost, epsilon, rho_a, rho_b=None, tolerance=1e-11, max_iterations=20000
):
    started = time.perf_counter()
    a = np.asarray(a, dtype=np.float64)
    b = np.asarray(b, dtype=np.float64)
    cost = np.asarray(cost, dtype=np.float64)
    rho_b = rho_a if rho_b is None else rho_b
    if a.ndim != 1 or b.ndim != 1 or cost.shape != (len(a), len(b)):
        raise ValueError("Mass/cost dimensions")
    if (
        not np.isfinite(a).all()
        or not np.isfinite(b).all()
        or not np.isfinite(cost).all()
        or np.any(a < 0)
        or np.any(b < 0)
        or a.sum() <= 0
        or b.sum() <= 0
    ):
        raise ValueError("Finite nonnegative masses with positive total required")
    if (
        not math.isfinite(epsilon)
        or epsilon <= 0
        or any(math.isnan(r) or r <= 0 for r in [rho_a, rho_b])
        or tolerance <= 0
        or max_iterations < 1
    ):
        raise ValueError("Invalid regularization/convergence configuration")
    balanced = math.isinf(rho_a) and math.isinf(rho_b)
    if balanced and abs(a.sum() - b.sum()) > 1e-12:
        raise ValueError("Balanced totals differ")
    active_a = a > 0
    active_b = b > 0
    aa = a[active_a]
    bb = b[active_b]
    C = cost[np.ix_(active_a, active_b)]
    kernel = -C / epsilon
    lu = np.zeros(len(aa))
    lv = np.zeros(len(bb))
    fa = 1.0 if math.isinf(rho_a) else rho_a / (rho_a + epsilon)
    fb = 1.0 if math.isinf(rho_b) else rho_b / (rho_b + epsilon)
    residual = math.inf
    for iteration in range(1, max_iterations + 1):
        new_u = fa * (np.log(aa) - logsumexp(kernel + lv[None, :], axis=1))
        new_v = fb * (np.log(bb) - logsumexp(kernel + new_u[:, None], axis=0))
        residual = max(
            float(np.max(np.abs(new_u - lu))), float(np.max(np.abs(new_v - lv)))
        )
        lu, lv = new_u, new_v
        if not np.isfinite(lu).all() or not np.isfinite(lv).all():
            raise FloatingPointError("Non-finite Sinkhorn dual iterate")
        if residual <= tolerance:
            break
    else:
        raise RuntimeError(
            f"Sinkhorn did not converge after {max_iterations} iterations; residual={residual}"
        )
    small = np.exp(lu[:, None] + kernel + lv[None, :])
    plan = np.zeros_like(cost)
    plan[np.ix_(active_a, active_b)] = small
    if not np.isfinite(plan).all():
        raise FloatingPointError("Non-finite transport plan")
    row = plan.sum(1)
    col = plan.sum(0)
    if balanced and max(np.max(np.abs(row - a)), np.max(np.abs(col - b))) > 1e-8:
        raise RuntimeError("Balanced marginal residual too large")
    return {
        "plan": plan.tolist(),
        "row_mass": row.tolist(),
        "column_mass": col.tolist(),
        "row_deficit": np.maximum(a - row, 0).tolist(),
        "row_excess": np.maximum(row - a, 0).tolist(),
        "column_deficit": np.maximum(b - col, 0).tolist(),
        "column_excess": np.maximum(col - b, 0).tolist(),
        "objective": objective(plan, cost, a, b, epsilon, rho_a, rho_b),
        "iterations": iteration,
        "residual": residual,
        "seconds": time.perf_counter() - started,
        "active_rows": np.flatnonzero(active_a).tolist(),
        "active_columns": np.flatnonzero(active_b).tolist(),
        "balanced": balanced,
        "epsilon": epsilon,
        "rho_a": None if math.isinf(rho_a) else rho_a,
        "rho_b": None if math.isinf(rho_b) else rho_b,
    }


def compare_pot(a, b, cost, epsilon, rho_a, rho_b=None, tolerance=1e-8):
    import ot

    rho_b = rho_a if rho_b is None else rho_b
    a = np.asarray(a, dtype=np.float64)
    b = np.asarray(b, dtype=np.float64)
    cost = np.asarray(cost, dtype=np.float64)
    ours = sinkhorn_unbalanced(a, b, cost, epsilon, rho_a, rho_b)
    ia = a > 0
    ib = b > 0
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        small, info = ot.unbalanced.sinkhorn_unbalanced(
            a[ia],
            b[ib],
            cost[np.ix_(ia, ib)],
            epsilon,
            (rho_a, rho_b),
            reg_type="entropy",
            numItermax=20000,
            stopThr=1e-13,
            log=True,
        )
    reference = np.zeros_like(cost)
    reference[np.ix_(ia, ib)] = small
    error = float(np.max(np.abs(reference - np.asarray(ours["plan"]))))
    if error > tolerance:
        raise AssertionError(f"POT plan discrepancy {error} > {tolerance}")
    return {
        "ours": ours,
        "reference_plan": reference.tolist(),
        "max_plan_error": error,
        "tolerance": tolerance,
        "reg_type": "entropy",
        "pot_version": ot.__version__,
        "pot_reported_total_cost": float(info["total_cost"]),
        "reference_objective_recomputed": objective(
            reference, cost, a, b, epsilon, rho_a, rho_b
        ),
        "warnings": [str(w.message) for w in caught],
    }

Paso 04 — Resolver planes con fusiones y divisiones

Relajación de los marginales y revisión

Error: Todas las filas se normalizan a 1 y después se denominan confianza

Corrección: Conservar las masas brutas, los marginales, los costes y las reglas de revisión calibradas.

Archivo src/ged_migration_taxonomies/run.py

import itertools, math
import numpy as np
from .config import read_json, save_json, sha
from .transport import sinkhorn_unbalanced, compare_pot
from .proposals import propose, metrics


def inspect_mechanism(config):
    a = [0.7, 0.3]
    b = [0.4, 0.6]
    cost = [[0.1, 0.9], [0.8, 0.2]]
    reference = compare_pot(a, b, cost, 0.1, 1.0, 0.7)
    permutation = sinkhorn_unbalanced(
        a[::-1], b[::-1], np.asarray(cost)[::-1, ::-1], 0.1, 1.0, 0.7
    )
    if (
        np.max(
            np.abs(
                np.asarray(reference["ours"]["plan"])
                - np.asarray(permutation["plan"])[::-1, ::-1]
            )
        )
        > 1e-10
    ):
        raise AssertionError("Permutation failure")
    zero = sinkhorn_unbalanced([1.0, 0.0], [0.4, 0.6], cost, 0.1, 1.0)
    save_json(
        "outputs/mechanism.json",
        {
            "a": a,
            "b": b,
            "cost": cost,
            "reference": reference,
            "permutation": permutation,
            "zero_mass": zero,
            "status": "PASS",
        },
    )


def compute_all(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    solutions = []
    for pair in pairs:
        for eps in config["epsilon_grid"]:
            solutions.append(
                {
                    "pair": pair["pair"],
                    "variant": "balanced",
                    "epsilon": eps,
                    "rho": None,
                    "solution": sinkhorn_unbalanced(
                        pair["a"],
                        pair["b"],
                        features[pair["pair"]]["cost"],
                        eps,
                        math.inf,
                    ),
                }
            )
            for rho in config["rho_grid"]:
                solutions.append(
                    {
                        "pair": pair["pair"],
                        "variant": "unbalanced",
                        "epsilon": eps,
                        "rho": rho,
                        "solution": sinkhorn_unbalanced(
                            pair["a"],
                            pair["b"],
                            features[pair["pair"]]["cost"],
                            eps,
                            rho,
                            tolerance=config["solver_tolerance"],
                            max_iterations=config["max_iterations"],
                        ),
                    }
                )
    save_json("outputs/plans.json", solutions)


def calibrate(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    plans = read_json("outputs/plans.json")
    gold = {g["pair"]: g for g in read_json("data/generated/labels/validation.json")}
    if set(gold) != set(range(12)):
        raise ValueError("Calibration gold only required")
    lookup = {
        (r["pair"], r["variant"], r["epsilon"], r["rho"]): r["solution"] for r in plans
    }
    choices = {}
    tables = {}
    for variant in ["top1", "balanced", "unbalanced"]:
        options = (
            [(None, None, None, None, t) for t in config["cosine_threshold_grid"]]
            if variant == "top1"
            else [
                (e, r, l, m, None)
                for e in config["epsilon_grid"]
                for r in ([None] if variant == "balanced" else config["rho_grid"])
                for l in config["link_mass_ratio_grid"]
                for m in config["minimum_row_mass_ratio_grid"]
            ]
        )
        rows = []
        for eps, rho, link, row, cos in options:
            total = {"tp": 0, "fp": 0, "fn": 0}
            for pair in pairs[:12]:
                solution = (
                    None
                    if variant == "top1"
                    else lookup[pair["pair"], variant, eps, rho]
                )
                result = metrics(
                    propose(
                        pair, features[pair["pair"]], variant, solution, link, row, cos
                    ),
                    gold[pair["pair"]],
                )
                for k in total:
                    total[k] += result[k]
            score = (
                2 * total["tp"] / (2 * total["tp"] + total["fp"] + total["fn"])
                if 2 * total["tp"] + total["fp"] + total["fn"]
                else 0.0
            )
            rows.append(
                {
                    "epsilon": eps,
                    "rho": rho,
                    "link_ratio": link,
                    "row_ratio": row,
                    "cosine_threshold": cos,
                    "counts": total,
                    "micro_F1": score,
                }
            )
        # Stable predefined grid order breaks ties, independent of test labels.
        winner = max(range(len(rows)), key=lambda i: (rows[i]["micro_F1"], -i))
        choices[variant] = dict(rows[winner], grid_index=winner)
        tables[variant] = rows
    save_json(
        "outputs/calibration.json",
        {
            "pairs": list(range(12)),
            "choices": choices,
            "grid": tables,
            "tie": "first in predefined grid",
            "test_gold_visible": False,
        },
    )


def evaluate(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    plans = read_json("outputs/plans.json")
    gold = {g["pair"]: g for g in read_json("data/generated/labels/gold.json")}
    selected = read_json("outputs/calibration.json")["choices"]
    lookup = {
        (r["pair"], r["variant"], r["epsilon"], r["rho"]): r["solution"] for r in plans
    }
    rows = []
    for pair in pairs:
        for variant, choice in selected.items():
            solution = (
                None
                if variant == "top1"
                else lookup[pair["pair"], variant, choice["epsilon"], choice["rho"]]
            )
            proposals = propose(
                pair,
                features[pair["pair"]],
                variant,
                solution,
                choice["link_ratio"],
                choice["row_ratio"],
                choice["cosine_threshold"],
            )
            rows.append(
                {
                    "pair": pair["pair"],
                    "split": pair["split"],
                    "variant": variant,
                    "proposals": proposals,
                    "metrics": metrics(proposals, gold[pair["pair"]]),
                    "choice": choice,
                }
            )
    save_json(
        "outputs/evaluation.json",
        {
            "rows": rows,
            "populations": {"calibration_pairs": 12, "test_pairs": 12},
            "applied": False,
            "semantics": "proposals only; transported mass is not probability",
        },
    )


def exercise(config):
    protected = {
        p: sha(p)
        for p in [
            "outputs/features.json",
            "outputs/calibration.json",
            "outputs/evaluation.json",
        ]
    }
    pair = read_json("data/generated/input/pages.json")[12]
    original = list(pair["a"])
    pair["a"][0] *= 10
    total = sum(pair["a"])
    pair["a"] = [v / total for v in pair["a"]]
    feature = read_json("outputs/features.json")["rows"][12]
    gold = read_json("data/generated/labels/gold.json")[12]
    choices = read_json("outputs/calibration.json")["choices"]
    rows = []
    for variant, c in choices.items():
        solution = (
            None
            if variant == "top1"
            else sinkhorn_unbalanced(
                pair["a"],
                pair["b"],
                feature["cost"],
                c["epsilon"],
                math.inf if variant == "balanced" else c["rho"],
            )
        )
        proposals = propose(
            pair,
            feature,
            variant,
            solution,
            c["link_ratio"],
            c["row_ratio"],
            c["cosine_threshold"],
        )
        rows.append(
            {
                "variant": variant,
                "solution": solution,
                "proposals": proposals,
                "metrics": metrics(proposals, gold),
            }
        )
    save_json(
        "outputs/exercise.json",
        {
            "pair": 12,
            "source_label": pair["source"][0]["label"],
            "original_a": original,
            "changed_a": pair["a"],
            "b": pair["b"],
            "multiplier": 10,
            "rows": rows,
            "protected_sha256": protected,
            "main_unchanged": all(sha(p) == v for p, v in protected.items()),
            "applied": False,
        },
    )

Paso 05 — Evaluar el mapeo, no solo su coste

Gold de varios a varios

Error: Un objetivo bajo se interpreta como una buena ontología

Corrección: Comparar con el gold y analizar los errores de definición junto con sus textos.

Archivo src/ged_migration_taxonomies/run.py

import itertools, math
import numpy as np
from .config import read_json, save_json, sha
from .transport import sinkhorn_unbalanced, compare_pot
from .proposals import propose, metrics


def inspect_mechanism(config):
    a = [0.7, 0.3]
    b = [0.4, 0.6]
    cost = [[0.1, 0.9], [0.8, 0.2]]
    reference = compare_pot(a, b, cost, 0.1, 1.0, 0.7)
    permutation = sinkhorn_unbalanced(
        a[::-1], b[::-1], np.asarray(cost)[::-1, ::-1], 0.1, 1.0, 0.7
    )
    if (
        np.max(
            np.abs(
                np.asarray(reference["ours"]["plan"])
                - np.asarray(permutation["plan"])[::-1, ::-1]
            )
        )
        > 1e-10
    ):
        raise AssertionError("Permutation failure")
    zero = sinkhorn_unbalanced([1.0, 0.0], [0.4, 0.6], cost, 0.1, 1.0)
    save_json(
        "outputs/mechanism.json",
        {
            "a": a,
            "b": b,
            "cost": cost,
            "reference": reference,
            "permutation": permutation,
            "zero_mass": zero,
            "status": "PASS",
        },
    )


def compute_all(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    solutions = []
    for pair in pairs:
        for eps in config["epsilon_grid"]:
            solutions.append(
                {
                    "pair": pair["pair"],
                    "variant": "balanced",
                    "epsilon": eps,
                    "rho": None,
                    "solution": sinkhorn_unbalanced(
                        pair["a"],
                        pair["b"],
                        features[pair["pair"]]["cost"],
                        eps,
                        math.inf,
                    ),
                }
            )
            for rho in config["rho_grid"]:
                solutions.append(
                    {
                        "pair": pair["pair"],
                        "variant": "unbalanced",
                        "epsilon": eps,
                        "rho": rho,
                        "solution": sinkhorn_unbalanced(
                            pair["a"],
                            pair["b"],
                            features[pair["pair"]]["cost"],
                            eps,
                            rho,
                            tolerance=config["solver_tolerance"],
                            max_iterations=config["max_iterations"],
                        ),
                    }
                )
    save_json("outputs/plans.json", solutions)


def calibrate(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    plans = read_json("outputs/plans.json")
    gold = {g["pair"]: g for g in read_json("data/generated/labels/validation.json")}
    if set(gold) != set(range(12)):
        raise ValueError("Calibration gold only required")
    lookup = {
        (r["pair"], r["variant"], r["epsilon"], r["rho"]): r["solution"] for r in plans
    }
    choices = {}
    tables = {}
    for variant in ["top1", "balanced", "unbalanced"]:
        options = (
            [(None, None, None, None, t) for t in config["cosine_threshold_grid"]]
            if variant == "top1"
            else [
                (e, r, l, m, None)
                for e in config["epsilon_grid"]
                for r in ([None] if variant == "balanced" else config["rho_grid"])
                for l in config["link_mass_ratio_grid"]
                for m in config["minimum_row_mass_ratio_grid"]
            ]
        )
        rows = []
        for eps, rho, link, row, cos in options:
            total = {"tp": 0, "fp": 0, "fn": 0}
            for pair in pairs[:12]:
                solution = (
                    None
                    if variant == "top1"
                    else lookup[pair["pair"], variant, eps, rho]
                )
                result = metrics(
                    propose(
                        pair, features[pair["pair"]], variant, solution, link, row, cos
                    ),
                    gold[pair["pair"]],
                )
                for k in total:
                    total[k] += result[k]
            score = (
                2 * total["tp"] / (2 * total["tp"] + total["fp"] + total["fn"])
                if 2 * total["tp"] + total["fp"] + total["fn"]
                else 0.0
            )
            rows.append(
                {
                    "epsilon": eps,
                    "rho": rho,
                    "link_ratio": link,
                    "row_ratio": row,
                    "cosine_threshold": cos,
                    "counts": total,
                    "micro_F1": score,
                }
            )
        # Stable predefined grid order breaks ties, independent of test labels.
        winner = max(range(len(rows)), key=lambda i: (rows[i]["micro_F1"], -i))
        choices[variant] = dict(rows[winner], grid_index=winner)
        tables[variant] = rows
    save_json(
        "outputs/calibration.json",
        {
            "pairs": list(range(12)),
            "choices": choices,
            "grid": tables,
            "tie": "first in predefined grid",
            "test_gold_visible": False,
        },
    )


def evaluate(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    plans = read_json("outputs/plans.json")
    gold = {g["pair"]: g for g in read_json("data/generated/labels/gold.json")}
    selected = read_json("outputs/calibration.json")["choices"]
    lookup = {
        (r["pair"], r["variant"], r["epsilon"], r["rho"]): r["solution"] for r in plans
    }
    rows = []
    for pair in pairs:
        for variant, choice in selected.items():
            solution = (
                None
                if variant == "top1"
                else lookup[pair["pair"], variant, choice["epsilon"], choice["rho"]]
            )
            proposals = propose(
                pair,
                features[pair["pair"]],
                variant,
                solution,
                choice["link_ratio"],
                choice["row_ratio"],
                choice["cosine_threshold"],
            )
            rows.append(
                {
                    "pair": pair["pair"],
                    "split": pair["split"],
                    "variant": variant,
                    "proposals": proposals,
                    "metrics": metrics(proposals, gold[pair["pair"]]),
                    "choice": choice,
                }
            )
    save_json(
        "outputs/evaluation.json",
        {
            "rows": rows,
            "populations": {"calibration_pairs": 12, "test_pairs": 12},
            "applied": False,
            "semantics": "proposals only; transported mass is not probability",
        },
    )


def exercise(config):
    protected = {
        p: sha(p)
        for p in [
            "outputs/features.json",
            "outputs/calibration.json",
            "outputs/evaluation.json",
        ]
    }
    pair = read_json("data/generated/input/pages.json")[12]
    original = list(pair["a"])
    pair["a"][0] *= 10
    total = sum(pair["a"])
    pair["a"] = [v / total for v in pair["a"]]
    feature = read_json("outputs/features.json")["rows"][12]
    gold = read_json("data/generated/labels/gold.json")[12]
    choices = read_json("outputs/calibration.json")["choices"]
    rows = []
    for variant, c in choices.items():
        solution = (
            None
            if variant == "top1"
            else sinkhorn_unbalanced(
                pair["a"],
                pair["b"],
                feature["cost"],
                c["epsilon"],
                math.inf if variant == "balanced" else c["rho"],
            )
        )
        proposals = propose(
            pair,
            feature,
            variant,
            solution,
            c["link_ratio"],
            c["row_ratio"],
            c["cosine_threshold"],
        )
        rows.append(
            {
                "variant": variant,
                "solution": solution,
                "proposals": proposals,
                "metrics": metrics(proposals, gold),
            }
        )
    save_json(
        "outputs/exercise.json",
        {
            "pair": 12,
            "source_label": pair["source"][0]["label"],
            "original_a": original,
            "changed_a": pair["a"],
            "b": pair["b"],
            "multiplier": 10,
            "rows": rows,
            "protected_sha256": protected,
            "main_unchanged": all(sha(p) == v for p, v in protected.items()),
            "applied": False,
        },
    )

Paso 06 — Modificar el peso de una categoría dominante

Sensibilidad a las distribuciones

Error: Se presenta el solucionador como si interpretara la intención del negocio

Corrección: Mostrar las hipótesis de carga y someter los cambios al archivo de revisión.

Archivo src/ged_migration_taxonomies/run.py

import itertools, math
import numpy as np
from .config import read_json, save_json, sha
from .transport import sinkhorn_unbalanced, compare_pot
from .proposals import propose, metrics


def inspect_mechanism(config):
    a = [0.7, 0.3]
    b = [0.4, 0.6]
    cost = [[0.1, 0.9], [0.8, 0.2]]
    reference = compare_pot(a, b, cost, 0.1, 1.0, 0.7)
    permutation = sinkhorn_unbalanced(
        a[::-1], b[::-1], np.asarray(cost)[::-1, ::-1], 0.1, 1.0, 0.7
    )
    if (
        np.max(
            np.abs(
                np.asarray(reference["ours"]["plan"])
                - np.asarray(permutation["plan"])[::-1, ::-1]
            )
        )
        > 1e-10
    ):
        raise AssertionError("Permutation failure")
    zero = sinkhorn_unbalanced([1.0, 0.0], [0.4, 0.6], cost, 0.1, 1.0)
    save_json(
        "outputs/mechanism.json",
        {
            "a": a,
            "b": b,
            "cost": cost,
            "reference": reference,
            "permutation": permutation,
            "zero_mass": zero,
            "status": "PASS",
        },
    )


def compute_all(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    solutions = []
    for pair in pairs:
        for eps in config["epsilon_grid"]:
            solutions.append(
                {
                    "pair": pair["pair"],
                    "variant": "balanced",
                    "epsilon": eps,
                    "rho": None,
                    "solution": sinkhorn_unbalanced(
                        pair["a"],
                        pair["b"],
                        features[pair["pair"]]["cost"],
                        eps,
                        math.inf,
                    ),
                }
            )
            for rho in config["rho_grid"]:
                solutions.append(
                    {
                        "pair": pair["pair"],
                        "variant": "unbalanced",
                        "epsilon": eps,
                        "rho": rho,
                        "solution": sinkhorn_unbalanced(
                            pair["a"],
                            pair["b"],
                            features[pair["pair"]]["cost"],
                            eps,
                            rho,
                            tolerance=config["solver_tolerance"],
                            max_iterations=config["max_iterations"],
                        ),
                    }
                )
    save_json("outputs/plans.json", solutions)


def calibrate(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    plans = read_json("outputs/plans.json")
    gold = {g["pair"]: g for g in read_json("data/generated/labels/validation.json")}
    if set(gold) != set(range(12)):
        raise ValueError("Calibration gold only required")
    lookup = {
        (r["pair"], r["variant"], r["epsilon"], r["rho"]): r["solution"] for r in plans
    }
    choices = {}
    tables = {}
    for variant in ["top1", "balanced", "unbalanced"]:
        options = (
            [(None, None, None, None, t) for t in config["cosine_threshold_grid"]]
            if variant == "top1"
            else [
                (e, r, l, m, None)
                for e in config["epsilon_grid"]
                for r in ([None] if variant == "balanced" else config["rho_grid"])
                for l in config["link_mass_ratio_grid"]
                for m in config["minimum_row_mass_ratio_grid"]
            ]
        )
        rows = []
        for eps, rho, link, row, cos in options:
            total = {"tp": 0, "fp": 0, "fn": 0}
            for pair in pairs[:12]:
                solution = (
                    None
                    if variant == "top1"
                    else lookup[pair["pair"], variant, eps, rho]
                )
                result = metrics(
                    propose(
                        pair, features[pair["pair"]], variant, solution, link, row, cos
                    ),
                    gold[pair["pair"]],
                )
                for k in total:
                    total[k] += result[k]
            score = (
                2 * total["tp"] / (2 * total["tp"] + total["fp"] + total["fn"])
                if 2 * total["tp"] + total["fp"] + total["fn"]
                else 0.0
            )
            rows.append(
                {
                    "epsilon": eps,
                    "rho": rho,
                    "link_ratio": link,
                    "row_ratio": row,
                    "cosine_threshold": cos,
                    "counts": total,
                    "micro_F1": score,
                }
            )
        # Stable predefined grid order breaks ties, independent of test labels.
        winner = max(range(len(rows)), key=lambda i: (rows[i]["micro_F1"], -i))
        choices[variant] = dict(rows[winner], grid_index=winner)
        tables[variant] = rows
    save_json(
        "outputs/calibration.json",
        {
            "pairs": list(range(12)),
            "choices": choices,
            "grid": tables,
            "tie": "first in predefined grid",
            "test_gold_visible": False,
        },
    )


def evaluate(config):
    pairs = read_json("data/generated/input/pages.json")
    features = {r["pair"]: r for r in read_json("outputs/features.json")["rows"]}
    plans = read_json("outputs/plans.json")
    gold = {g["pair"]: g for g in read_json("data/generated/labels/gold.json")}
    selected = read_json("outputs/calibration.json")["choices"]
    lookup = {
        (r["pair"], r["variant"], r["epsilon"], r["rho"]): r["solution"] for r in plans
    }
    rows = []
    for pair in pairs:
        for variant, choice in selected.items():
            solution = (
                None
                if variant == "top1"
                else lookup[pair["pair"], variant, choice["epsilon"], choice["rho"]]
            )
            proposals = propose(
                pair,
                features[pair["pair"]],
                variant,
                solution,
                choice["link_ratio"],
                choice["row_ratio"],
                choice["cosine_threshold"],
            )
            rows.append(
                {
                    "pair": pair["pair"],
                    "split": pair["split"],
                    "variant": variant,
                    "proposals": proposals,
                    "metrics": metrics(proposals, gold[pair["pair"]]),
                    "choice": choice,
                }
            )
    save_json(
        "outputs/evaluation.json",
        {
            "rows": rows,
            "populations": {"calibration_pairs": 12, "test_pairs": 12},
            "applied": False,
            "semantics": "proposals only; transported mass is not probability",
        },
    )


def exercise(config):
    protected = {
        p: sha(p)
        for p in [
            "outputs/features.json",
            "outputs/calibration.json",
            "outputs/evaluation.json",
        ]
    }
    pair = read_json("data/generated/input/pages.json")[12]
    original = list(pair["a"])
    pair["a"][0] *= 10
    total = sum(pair["a"])
    pair["a"] = [v / total for v in pair["a"]]
    feature = read_json("outputs/features.json")["rows"][12]
    gold = read_json("data/generated/labels/gold.json")[12]
    choices = read_json("outputs/calibration.json")["choices"]
    rows = []
    for variant, c in choices.items():
        solution = (
            None
            if variant == "top1"
            else sinkhorn_unbalanced(
                pair["a"],
                pair["b"],
                feature["cost"],
                c["epsilon"],
                math.inf if variant == "balanced" else c["rho"],
            )
        )
        proposals = propose(
            pair,
            feature,
            variant,
            solution,
            c["link_ratio"],
            c["row_ratio"],
            c["cosine_threshold"],
        )
        rows.append(
            {
                "variant": variant,
                "solution": solution,
                "proposals": proposals,
                "metrics": metrics(proposals, gold),
            }
        )
    save_json(
        "outputs/exercise.json",
        {
            "pair": 12,
            "source_label": pair["source"][0]["label"],
            "original_a": original,
            "changed_a": pair["a"],
            "b": pair["b"],
            "multiplier": 10,
            "rows": rows,
            "protected_sha256": protected,
            "main_unchanged": all(sha(p) == v for p, v in protected.items()),
            "applied": False,
        },
    )

Fuentes

Chizat et al., Scaling Algorithms for Unbalanced Transport Problems describe la familia de escalados. POT 0.9.7 documenta el solucionador de referencia y su elección de regularización. SKOS Reference distingue las relaciones de organización y de mapeo; las propuestas del ejercicio no las afirman. E5 fijado.

Interpretación del recorrido de desarrollo

En los 12 pares de prueba, el F1 medio por par es 0,875 para Top 1, 0,591636 para el transporte equilibrado y 0,742633 para el transporte no equilibrado. Los totales TP/FP/FN son, respectivamente, 84/0/24, 49/9/59 y 74/18/34. Los dos transportes crean respectivamente 2 y 1 enlaces falsos en conceptos sin equivalente; Top 1 crea 0. Los 192 planes convergen y el control POT 2×2 presenta un error máximo de 2,4871e-12. El solucionador está verificado, sin mejora de pertinencia frente a la referencia. Para este corpus, Top 1 es la opción respaldada por la calidad medida; una necesidad de distribución global de las masas exigiría una nueva evaluación de negocio.

El ejercicio multiplica por diez la carga de la fuente 0 y después renormaliza a; los costes, el marginal de destino y los umbrales permanecen fijos. Una variación de las masas de transporte no representa una variación de la probabilidad de enlace. Los dos mapas de calor utilizan escalas de color propias: deben compararse los valores de las barras, no solo los colores.

Observaciones de desarrollo

Valores recalculados a partir de las salidas brutas de este recorrido. No son umbrales de mejora; el recorrido del ZIP los recalcula sobre las mismas poblaciones.

{
  "status": "PASS",
  "pairs": 24,
  "calibration_pairs": 12,
  "test_pairs": 12,
  "source_concepts": 192,
  "target_concepts": 240,
  "pieces": 1536,
  "plans": 192,
  "calibration_choices": {
    "top1": {
      "epsilon": null,
      "rho": null,
      "link_ratio": null,
      "row_ratio": null,
      "cosine_threshold": 0.95,
      "counts": {
        "tp": 84,
        "fp": 0,
        "fn": 24
      },
      "micro_F1": 0.875,
      "grid_index": 19
    },
    "balanced": {
      "epsilon": 0.03,
      "rho": null,
      "link_ratio": 0.3,
      "row_ratio": 0.1,
      "cosine_threshold": null,
      "counts": {
        "tp": 66,
        "fp": 3,
        "fn": 42
      },
      "micro_F1": 0.7457627118644068,
      "grid_index": 4
    },
    "unbalanced": {
      "epsilon": 0.03,
      "rho": 0.1,
      "link_ratio": 0.3,
      "row_ratio": 0.1,
      "cosine_threshold": null,
      "counts": {
        "tp": 83,
        "fp": 4,
        "fn": 25
      },
      "micro_F1": 0.8512820512820513,
      "grid_index": 4
    }
  },
  "test_metrics": [
    {
      "pair": 12,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 12,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 4,
      "tp": 4,
      "fp": 0,
      "fn": 5,
      "precision": 1.0,
      "recall": 0.4444444444444444,
      "F1": 0.6153846153846154,
      "covered_source_concepts": 4,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 3,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 12,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 6,
      "tp": 6,
      "fp": 0,
      "fn": 3,
      "precision": 1.0,
      "recall": 0.6666666666666666,
      "F1": 0.8,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 13,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 13,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 4,
      "tp": 4,
      "fp": 0,
      "fn": 5,
      "precision": 1.0,
      "recall": 0.4444444444444444,
      "F1": 0.6153846153846154,
      "covered_source_concepts": 4,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 3,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 13,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 8,
      "tp": 7,
      "fp": 1,
      "fn": 2,
      "precision": 0.875,
      "recall": 0.7777777777777778,
      "F1": 0.8235294117647058,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 14,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 14,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 5,
      "tp": 5,
      "fp": 0,
      "fn": 4,
      "precision": 1.0,
      "recall": 0.5555555555555556,
      "F1": 0.7142857142857143,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 2,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 14,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 9,
      "tp": 7,
      "fp": 2,
      "fn": 2,
      "precision": 0.7777777777777778,
      "recall": 0.7777777777777778,
      "F1": 0.7777777777777778,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 15,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 15,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 6,
      "tp": 3,
      "fp": 3,
      "fn": 6,
      "precision": 0.5,
      "recall": 0.3333333333333333,
      "F1": 0.4,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 1,
      "without_equivalent_concepts": 1,
      "review_rows": 2,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 15,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 11,
      "tp": 6,
      "fp": 5,
      "fn": 3,
      "precision": 0.5454545454545454,
      "recall": 0.6666666666666666,
      "F1": 0.6,
      "covered_source_concepts": 8,
      "source_concepts": 8,
      "false_links_without_equivalent": 1,
      "without_equivalent_concepts": 1,
      "review_rows": 0,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 16,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 16,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 4,
      "tp": 4,
      "fp": 0,
      "fn": 5,
      "precision": 1.0,
      "recall": 0.4444444444444444,
      "F1": 0.6153846153846154,
      "covered_source_concepts": 4,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 4,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 16,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 6,
      "tp": 6,
      "fp": 0,
      "fn": 3,
      "precision": 1.0,
      "recall": 0.6666666666666666,
      "F1": 0.8,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 17,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 17,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 5,
      "tp": 5,
      "fp": 0,
      "fn": 4,
      "precision": 1.0,
      "recall": 0.5555555555555556,
      "F1": 0.7142857142857143,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 2,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 17,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 18,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 18,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 5,
      "tp": 4,
      "fp": 1,
      "fn": 5,
      "precision": 0.8,
      "recall": 0.4444444444444444,
      "F1": 0.5714285714285714,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 3,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 18,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 8,
      "tp": 6,
      "fp": 2,
      "fn": 3,
      "precision": 0.75,
      "recall": 0.6666666666666666,
      "F1": 0.7058823529411765,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 19,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 19,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 5,
      "tp": 5,
      "fp": 0,
      "fn": 4,
      "precision": 1.0,
      "recall": 0.5555555555555556,
      "F1": 0.7142857142857143,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 2,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 19,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 8,
      "tp": 7,
      "fp": 1,
      "fn": 2,
      "precision": 0.875,
      "recall": 0.7777777777777778,
      "F1": 0.8235294117647058,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 20,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 20,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 4,
      "tp": 4,
      "fp": 0,
      "fn": 5,
      "precision": 1.0,
      "recall": 0.4444444444444444,
      "F1": 0.6153846153846154,
      "covered_source_concepts": 4,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 3,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 20,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 6,
      "tp": 6,
      "fp": 0,
      "fn": 3,
      "precision": 1.0,
      "recall": 0.6666666666666666,
      "F1": 0.8,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 2,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 21,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 21,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 6,
      "tp": 5,
      "fp": 1,
      "fn": 4,
      "precision": 0.8333333333333334,
      "recall": 0.5555555555555556,
      "F1": 0.6666666666666666,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 3,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 21,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 8,
      "tp": 6,
      "fp": 2,
      "fn": 3,
      "precision": 0.75,
      "recall": 0.6666666666666666,
      "F1": 0.7058823529411765,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 22,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 22,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 5,
      "tp": 3,
      "fp": 2,
      "fn": 6,
      "precision": 0.6,
      "recall": 0.3333333333333333,
      "F1": 0.42857142857142855,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 1,
      "without_equivalent_concepts": 1,
      "review_rows": 3,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 22,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 9,
      "tp": 6,
      "fp": 3,
      "fn": 3,
      "precision": 0.6666666666666666,
      "recall": 0.6666666666666666,
      "F1": 0.6666666666666666,
      "covered_source_concepts": 6,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 23,
      "variant": "top1",
      "true_links": 9,
      "proposed_links": 7,
      "tp": 7,
      "fp": 0,
      "fn": 2,
      "precision": 1.0,
      "recall": 0.7777777777777778,
      "F1": 0.875,
      "covered_source_concepts": 7,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 1,
      "excluded_rows": 0,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 23,
      "variant": "balanced",
      "true_links": 9,
      "proposed_links": 5,
      "tp": 3,
      "fp": 2,
      "fn": 6,
      "precision": 0.6,
      "recall": 0.3333333333333333,
      "F1": 0.42857142857142855,
      "covered_source_concepts": 4,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 3,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    },
    {
      "pair": 23,
      "variant": "unbalanced",
      "true_links": 9,
      "proposed_links": 6,
      "tp": 4,
      "fp": 2,
      "fn": 5,
      "precision": 0.6666666666666666,
      "recall": 0.4444444444444444,
      "F1": 0.5333333333333333,
      "covered_source_concepts": 5,
      "source_concepts": 8,
      "false_links_without_equivalent": 0,
      "without_equivalent_concepts": 1,
      "review_rows": 2,
      "excluded_rows": 1,
      "applied": false,
      "skos_exactMatch_assertions": 0
    }
  ],
  "POT_max_error": 2.4871216197652757e-12,
  "proposals_applied": false
}

Los resultados observados, su alcance y el experimento propuesto.

Compartir este artículo