Durante una migrazione di gestione documentale, alcune rubriche cambiano nome, si dividono, si fondono o non hanno un equivalente. Un abbinamento forzato uno-a-uno crea corrispondenze fuorvianti e talvolta sposta documenti in una categoria inadeguata.
Una matrice spiegabile propone collegamenti multipli con costi, masse e avvisi, quindi esporta un file per la revisione. Non viene creato automaticamente alcun skos:exactMatch né alcuno spostamento di documenti.
Scaricare il progetto completo, revisione r001
Un corpus di corrispondenze multiple
Ventiquattro coppie di piani hanno ciascuna 8 concetti sorgente e 10 destinazioni, per un totale di 192 e 240 concetti. Dodici domini servono alla calibrazione e altri dodici al test. I domini e gli involucri testuali sono distinti, ma i prototipi semantici delle relazioni e i sinonimi sono condivisi: non è un esperimento completamente fuori distribuzione. Questa scelta rende i meccanismi leggibili; le prestazioni non dimostrano una generalizzazione aziendale.
Ogni coppia possiede 64 note originali di carico. Le loro due assegnazioni di classificazione sono simulate indipendentemente e producono a e b come conteggi/64; non sono annotazioni di corrispondenza. Tutti i concetti del percorso principale hanno una massa positiva. Il gold contiene relazioni uno-a-molti, molti-a-uno e una rubrica senza equivalente. Rimane in un file separato, mai nei costi.
Le relazioni broader restano interne a un piano; il codice rifiuta duplicati, genitori assenti e cicli. Questo laboratorio adotta questa convenzione aciclica locale; non pretende di definire tutti i vincoli generali di SKOS. Nessuna esportazione dichiara skos:exactMatch e non viene applicata alcuna migrazione.
E5: significato e contesto
E5-small, fissato alla revisione 614241f622f53c4eeff9890bdc4f31cfecc418b3, codifica etichetta+definizione e una seconda stringa di contesto degli antenati. Il prefisso query: è simmetrico per sorgente e destinazione. Pooling mascherato, token speciali reali inclusi, quindi normalizzazione L2 in 384 dimensioni; il codice rifiuta più di 512 token e verifica tutti i file e i tensori caricati.
Una radice usa la stessa frase esplicita di contesto della radice, quindi radice/radice ha un contesto identico. Il costo è Cij=1−cos(ei,ej)+0,25(1−cos(ai,aj)). Il fattore 0,25 è fisso. I coseni calcolati con i vettori FP32 sono accumulati in FP64, poi limitati a [-1,1] per l’arrotondamento. I costi non conoscono il gold.
Tre metodi, tre interpretazioni
Il top 1 per coseno sceglie la destinazione testualmente più vicina, con una soglia scelta durante la calibrazione. Ignora le masse, quindi resta invariato nell’esercizio sulle masse. Il trasporto bilanciato impone i suoi margini a e b: può spostare un carico verso una destinazione semanticamente mediocre per soddisfare tali vincoli.
Il trasporto non bilanciato minimizza somma(ΠC)+epsilon somma(Π(logΠ−1))+rho_a KL(Π1||a)+rho_b KL(Πᵀ1||b). La KL generalizzata vale somma(p log(p/q)−p+q), con lo zero trattato esplicitamente. Le penalità di margine autorizzano deficit ed eccesso; un margine può superare il proprio carico iniziale. Nessuna di queste quantità è una probabilità di corrispondenza.
Il nucleo logaritmico vale −C/epsilon. Si alternano logu=fa(loga−logsumexp(logK+logv)) e logv=fb(logb−logsumexp(logKᵀ+logu)), con fa=rho_a/(rho_a+epsilon). Rho infinito dà fa=1 per il caso bilanciato. La stabilizzazione sottrae il massimo in logsumexp. Le masse nulle sono tolte dal sottoproblema e poi reinserite come righe o colonne nulle. Totali tutti nulli, dati non finiti o parametri non validi vengono rifiutati; l’assenza di convergenza è un errore leggibile.
La convergenza richiede una variazione massima dei potenziali logaritmici non superiore a 1e-11, in meno di 20000 iterazioni, e margini bilanciati a 1e-8. Il piccolo caso 2×2 è confrontato con POT 0.9.7 in FP64, con reg_type='entropy' esplicito e tolleranza del piano a 1e-8. L’impostazione kl predefinita usa un altro riferimento di regolarizzazione. I due piani sono valutati con la stessa formula dell’obiettivo; un total_cost riportato da POT non viene assimilato ciecamente a questa convenzione di costante.
Calibrare prima del test
Epsilon percorre 0,03 e 0,1; rho 0,1, 1 e 10. Le soglie di collegamento riguardano Πij/ai, e una soglia di revisione riguarda somma_jΠij/ai. Questi rapporti possono superare 1. Non sono normalizzati come probabilità né chiamati confidenza. La compatibilità business_scope filtra le proposte finanziarie incompatibili come EXCLUDED; il risolutore usa esso stesso costi finiti e questa regola non è un vincolo di trasporto nascosto.
Le griglie e il loro ordine sono fissati. Sulle 12 coppie di calibrazione, ogni metodo sceglie il micro-F1 dei collegamenti multipli, poi il primo elemento della griglia in caso di parità. Le 12 coppie di test non scelgono alcuna soglia, epsilon o rho. I piani di test possono essere calcolati senza gold; le loro metriche sono calcolate solo dopo la calibrazione.
Le proposte sono confrontate con l’insieme gold dei collegamenti: TP/FP/FN, precisione, richiamo, F1, copertura dei concetti sorgente, falsi collegamenti sulla rubrica senza equivalente, REVIEW e EXCLUDED. Gli output indicano margini, deficit, eccessi, obiettivo e residuo del risolutore. La durata del risolutore CPU è separata dalla codifica GPU. Un punteggio migliore su queste piccole tassonomie non giustifica una migrazione automatica.
Esercizio con embedding fissati
La coppia di test 12 è scelta prima dei risultati. Moltiplicate per dieci la massa di Visite atelier, poi rinormalizzate a, senza cambiare b, gli embedding o i parametri scelti. Ricalcolate il trasporto bilanciato e non bilanciato. Il top 1 resta testuale; i piani possono cambiare. Leggete i valori osservati senza pretendere un miglioramento, quindi confrontate le definizioni prima di qualsiasi decisione umana di migrazione. Gli output principali restano protetti da hash.
Fase 01 — Caricare i piani come concetti identificati
SKOS, etichette e gerarchia
Errore: i concetti vengono fusi in base alla sola etichetta
Correzione: conservare URI e piano d’origine; rifiutare i cicli nel minimo.
File 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.",
},
)
Fase 02 — Costruire una prima corrispondenza testuale
Embedding e costo simmetrico
Errore: le corrispondenze gold servono al contesto degli antenati
Correzione: costruire il contesto solo a partire da broader, senza accesso al mapping gold.
File 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"],
},
)
Fase 03 — Deducere le scalature del trasporto
Entropia, KL dei margini e spazio logaritmico
Errore: confrontare reg_type='kl' con il calcolo entropico del laboratorio
Correzione: passare reg_type='entropy' nel riferimento e spiegare questa differenza di convenzione.
File 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],
}
Fase 04 — Risolvere piani con fusioni e divisioni
Rilassamento dei margini e revisione
Errore: tutte le righe sono normalizzate a 1 e poi chiamate confidenza
Correzione: conservare masse grezze, margini, costi e regole di revisione calibrate.
File 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,
},
)
Fase 05 — Valutare il mapping, non soltanto il suo costo
Gold molti-a-molti
Errore: un objective basso viene interpretato come una buona ontologia
Correzione: confrontare con il gold e analizzare gli errori di definizione con i relativi testi.
File 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,
},
)
Fase 06 — Modificare il peso di una rubrica dominante
Sensibilità alle distribuzioni
Errore: il risolutore viene presentato come se leggesse l’intenzione aziendale
Correzione: mostrare le ipotesi di carico e sottoporre le modifiche al file di revisione.
File 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,
},
)
Fonti
Chizat et al., Scaling Algorithms for Unbalanced Transport Problems descrive la famiglia degli algoritmi di scalatura. POT 0.9.7 documenta il risolutore di riferimento e la sua scelta di regolarizzazione. SKOS Reference distingue le relazioni di organizzazione e di mapping; le proposte del laboratorio non le affermano. E5 fissato.
Interpretazione del percorso di sviluppo
Sulle 12 coppie di test, l’F1 medio per coppia è 0,875 per Top 1, 0,591636 per il trasporto bilanciato e 0,742633 per il trasporto non bilanciato. I totali TP/FP/FN sono rispettivamente 84/0/24, 49/9/59 e 74/18/34. I due trasporti creano rispettivamente 2 e 1 falsi collegamenti su concetti senza equivalente; Top 1 ne crea 0. I 192 piani convergono e il controllo POT 2×2 ha un errore massimo di 2,4871e-12. Il risolutore è verificato, senza guadagno di pertinenza rispetto alla baseline. Per questo corpus, Top 1 è la scelta sostenuta dalla qualità misurata; un’esigenza di ripartizione globale delle masse richiederebbe una nuova valutazione aziendale.
L’esercizio moltiplica per dieci il carico della sorgente 0, poi rinormalizza a; i costi, il margine di destinazione e le soglie restano fissi. Una variazione delle masse di trasporto non rappresenta una variazione della probabilità di collegamento. Le due heatmap mostrano le proprie scale di colore: confrontate i valori delle barre, non solo i colori.
Osservazioni sullo sviluppo
Valori ricalcolati dalle uscite grezze di questo percorso. Non sono soglie di guadagno; il percorso dallo ZIP li ricalcola sulle stesse popolazioni.
{
"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
}