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