Algorithms¶
base ¶
Abstract interface implemented by every linking algorithm.
BaseLinker ¶
Bases: ABC
Common interface for Entity Alignment, Entity Linking, WSD and WSA.
Subclasses implement a single task by consuming two entity datasets (whose shape depends on the task), an optional supporting graph, and a blocking strategy used to restrict the candidate search space.
link
abstractmethod
¶
link(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
graph: Graph = None,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> list[AlignmentResult]
Link entities in dataset1 to entities in dataset2.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities (e.g. mentions, source KG entities). |
required |
dataset2
|
list[Entity] | EntitySource
|
Target entities (e.g. KB entries, target KG entities),
or an EntitySource
wrapping a target too large to materialize as a
|
required |
graph
|
Graph
|
Optional supporting graph as a triple list, a
|
None
|
blocking
|
BlockingStrategy
|
Strategy used to generate candidate pairs before scoring. |
DEFAULT_BLOCKING
|
Returns:
| Type | Description |
|---|---|
list[AlignmentResult]
|
A list of predicted links. |
Source code in src/linkingtk/algorithms/base.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
String-similarity baseline¶
string_similarity ¶
General string-similarity baseline linker.
Scores each blocked candidate pair by the similarity of a configurable text field from either side (e.g. a mention's context against a candidate's gloss, or two entities' labels), then ranks candidates per source entity. This is a general-purpose baseline for Entity Alignment, Entity Linking and Word Sense Alignment; the classic Lesk algorithm for WSD (linkingtk.algorithms.wsd.lesk.LeskLinker) is implemented as a preconfigured instance of this class.
StringSimilarityLinker ¶
StringSimilarityLinker(
source_field: Field = "label",
target_field: Field = "label",
metric: Metric = "jaccard",
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Baseline linker that scores candidates by string similarity.
For each blocked (entity1, entity2) pair, extracts source_field
text from entity1 and target_field text from entity2, and
scores the pair with metric (higher is always better, regardless
of metric). For each source entity, the highest-scoring candidate is
returned as the predicted link, with the rest kept as ranked
alternatives.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_field
|
Field
|
Which text to extract from each |
'label'
|
target_field
|
Field
|
Which text to extract from each |
'label'
|
metric
|
Metric
|
|
'jaccard'
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher (each source entity's highest-scoring candidate, independently — multiple sources may map to the same target). Pass OptimalMatcher for a globally optimal one-to-one assignment instead. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/string_similarity.py
97 98 99 100 101 102 103 104 105 106 107 | |
jaccard ¶
jaccard(text1: str, text2: str) -> float
Token-set Jaccard similarity, normalized to [0, 1].
Source code in src/linkingtk/algorithms/string_similarity.py
43 44 45 46 47 | |
levenshtein_similarity ¶
levenshtein_similarity(text1: str, text2: str) -> float
Character-level similarity derived from normalized edit distance, in [0, 1].
Source code in src/linkingtk/algorithms/string_similarity.py
50 51 52 53 54 55 56 | |
word_overlap ¶
word_overlap(text1: str, text2: str) -> float
Count of shared tokens, as used by the classic Lesk algorithm.
Source code in src/linkingtk/algorithms/string_similarity.py
38 39 40 | |
Feature-based classifier¶
feature_classifier ¶
Feature-based classifier linker for Entity Alignment and Word Sense Alignment.
Scores each blocked candidate pair with a classical ML classifier trained on hand-crafted similarity features, rather than a single fixed metric — in the style of EntMatcher (https://github.com/DexterZeng/EntMatcher). EntMatcher itself is a research repo, not a dependency of this project; the idea reused here is that a globally optimal one-to-one assignment (OptimalMatcher) can outperform independent per-source argmax matching, regardless of what produces the underlying similarity score.
Classifier ¶
Bases: Protocol
Structural type for a fit/predict_proba classifier (e.g. scikit-learn's).
FeatureClassifierLinker ¶
FeatureClassifierLinker(
features: list[tuple[str, FeatureFn]] | None = None,
vectorizer: Vectorizer | None = None,
classifier: Classifier | None = None,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Scores candidate pairs with a classical ML classifier over hand-crafted features.
Unlike StringSimilarityLinker
(a single fixed metric), this combines several similarity signals —
DEFAULT_FEATURES (string-overlap/edit-distance metrics on the
label and description fields, reused from
linkingtk.algorithms.string_similarity) plus a TF-IDF cosine
similarity feature over combined label+description text — into a
feature vector scored by a trainable classifier. Must be
fit
before link can be called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
features
|
list[tuple[str, FeatureFn]] | None
|
|
None
|
vectorizer
|
Vectorizer | None
|
Any object exposing scikit-learn's
|
None
|
classifier
|
Classifier | None
|
Any object exposing scikit-learn's |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final
links. Defaults to
GreedyMatcher (each source
entity's highest-scoring candidate, independently, like
|
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/feature_classifier.py
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
blocking: BlockingStrategy = DEFAULT_BLOCKING,
negatives: list[tuple[Entity, Entity]] | None = None,
negative_ratio: int = 1,
random_state: int | None = None,
) -> FeatureClassifierLinker
Train the classifier on candidate pairs from blocking.
Positive examples are the candidate pairs found in
ground_truth. Negative examples default to a simple,
self-contained strategy: the remaining candidate pairs, sampled
uniformly at random (up to negative_ratio times the number of
positives). Pass negatives explicitly — e.g. from
sample_hard_negatives
— for more deliberate negative mining instead; when given,
negative_ratio and random_state are ignored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. |
required |
dataset2
|
list[Entity]
|
Target entities. |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
blocking
|
BlockingStrategy
|
Strategy used to generate training candidate pairs. |
DEFAULT_BLOCKING
|
negatives
|
list[tuple[Entity, Entity]] | None
|
Explicit negative |
None
|
negative_ratio
|
int
|
Maximum number of negative examples sampled
per positive example. Ignored if |
1
|
random_state
|
int | None
|
Seed for negative-example sampling. Ignored if
|
None
|
Returns:
| Type | Description |
|---|---|
FeatureClassifierLinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If no |
Source code in src/linkingtk/algorithms/feature_classifier.py
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
LLM-based linker¶
llm ¶
General-purpose linker that prompts an LLM to decide/score links.
Usable across Entity Alignment, Entity Linking, Word Sense Disambiguation
and Word Sense Alignment -- selected via task, the same way
StringSimilarityLinker
is one general-purpose class rather than four task-specific subclasses.
Talks to an LLM through LlmClient (#60),
so this module never imports openai/anthropic itself.
LlmBaseLinker ¶
LlmBaseLinker(
client: LlmClient,
task: Task = "el",
matching: Matcher = DEFAULT_MATCHER,
max_tokens: int = 2048,
)
Bases: BaseLinker
Prompts an LLM to score/rank each source entity's blocked candidates.
Unlike StringSimilarityLinker, which scores each candidate pair independently, this linker makes one LLM call per source entity, showing all of its blocked candidates at once via LlmClient.complete_structured -- cheaper than one call per pair, and gives the model real comparative context instead of siloed yes/no judgments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
LlmClient
|
An already-constructed LlmClient
(e.g. from create_client).
This class never imports |
required |
task
|
Task
|
Which PromptTemplate
instruction to use -- |
'el'
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final
links, same as |
DEFAULT_MATCHER
|
max_tokens
|
int
|
Forwarded to |
2048
|
Note
graph is accepted for interface compliance but not used, same as
StringSimilarityLinker. LLM calls are made sequentially, one per
source entity with at least one blocked candidate.
Source code in src/linkingtk/algorithms/llm.py
85 86 87 88 89 90 91 92 93 94 95 | |
PromptTemplate
dataclass
¶
PromptTemplate(instruction: str)
A task's instruction to the LLM, prepended to a source entity + its candidates.
Attributes:
| Name | Type | Description |
|---|---|---|
instruction |
str
|
The system-message text describing the task and the
expected judgment -- the only part that varies per task. Entity
rendering itself ( |
_llm_prompting ¶
Shared LLM-prompting building blocks for LlmBaseLinker and LlmRerankerLinker.
Extracted from llm.py (#21) so #23's reranker reuses the exact same
per-task instructions, entity rendering, structured-output schema, and
hallucinated-id recovery, rather than a parallel copy.
PromptTemplate
dataclass
¶
PromptTemplate(instruction: str)
A task's instruction to the LLM, prepended to a source entity + its candidates.
Attributes:
| Name | Type | Description |
|---|---|---|
instruction |
str
|
The system-message text describing the task and the
expected judgment -- the only part that varies per task. Entity
rendering itself ( |
LLM re-ranker¶
llm_reranker ¶
LLM re-ranking layer over an already-scored bi-encoder/cross-encoder candidate set.
Filed as #23: a cheap, already-trained ReFinEDLinker/ BlinkLinker/ GlossBertLinker/ EwiserLinker/ EscLinker narrows blocked candidates to a scored top-k first; only that narrow window is then shown to an LLM for re-ranking, unlike LlmBaseLinker (#21), which sends an LLM every blocked candidate with no first-stage narrowing. No specific paper is being ported here (unlike ChatEALinker, #22) -- generalizes the same "retrieve cheap, then let an LLM re-rank only the top-k" idea to Entity Linking and Word Sense Disambiguation.
CandidateScorer ¶
Bases: Protocol
What LlmRerankerLinker needs from its base_linker.
ReFinEDLinker, BlinkLinker, GlossBertLinker, EwiserLinker, and
EscLinker all implement this -- their own link() calls it directly
too, so a reranked source entity's top-k is always built from the same
scores link() would otherwise resolve straight into matching.match().
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity id: (target_id, score), unsorted.
Source code in src/linkingtk/algorithms/llm_reranker.py
52 53 54 55 56 57 58 | |
LlmRerankerLinker ¶
LlmRerankerLinker(
base_linker: CandidateScorer,
client: LlmClient,
task: Task = "el",
top_k: int = 10,
threshold: float | None = None,
matching: Matcher = DEFAULT_MATCHER,
max_tokens: int = 2048,
)
Bases: BaseLinker
Re-ranks a base_linker's top-top_k blocked candidates with an LLM.
No fit() -- like LlmBaseLinker,
the LLM needs no training; only base_linker's own model does, via
whatever training path it normally uses (e.g.
Trainer,
CrossEncoderTrainer,
or a loaded checkpoint), done by the caller before constructing this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_linker
|
CandidateScorer
|
An already-fitted/loaded
CandidateScorer
-- any of |
required |
client
|
LlmClient
|
An already-constructed LlmClient.
This class never imports |
required |
task
|
Task
|
Which PromptTemplate
instruction to use -- typically |
'el'
|
top_k
|
int
|
How many of |
10
|
threshold
|
float | None
|
Confidence shortcut -- if |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. |
DEFAULT_MATCHER
|
max_tokens
|
int
|
Forwarded to |
2048
|
Note
graph is accepted for interface compliance but not used, same as
LlmBaseLinker. blocking is applied twice per link() call --
once inside base_linker.score_candidates(), once again here to
recover real Entity objects for prompt rendering (scores alone
aren't enough to build a prompt). BlockingStrategys are cheap
index lookups next to a transformer forward pass, so this isn't
worth threading Entity objects through CandidateScorer's return
type to avoid.
Source code in src/linkingtk/algorithms/llm_reranker.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
Entity Alignment¶
ea ¶
Entity Alignment (EA) algorithms.
EA finds matches between two knowledge graphs, often across languages. Entities typically carry labels and descriptions but rarely a context, and a supporting graph is normally available. Related systems: OpenEA, Simple-HHEA, ChatEA, ProLEA, EntMatcher.
AliNetLinker ¶
AliNetLinker(
layer_dims: list[int] | None = None,
num_epochs: int = 500,
learning_rate: float = 0.001,
batch_size: int = 3000,
neg_triple_num: int = 10,
neg_margin: float = 1.5,
neg_margin_balance: float = 0.1,
sim_th: float = 0.0,
bootstrap_start_epoch: int = 20,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via AliNet's gated multi-hop GCN/attention embeddings.
Must be fit before link can be called. See the module docstring for what's ported vs. deliberately deviated from OpenEA.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_dims
|
list[int] | None
|
Layer width sequence -- |
None
|
num_epochs
|
int
|
Training epochs. OpenEA's config allows up to |
500
|
learning_rate
|
float
|
Adam's learning rate. OpenEA's published value is
|
0.001
|
batch_size
|
int
|
Positive pairs sampled per training step (with
replacement, matching OpenEA). OpenEA's published value is
|
3000
|
neg_triple_num
|
int
|
Negatives drawn per positive pair ( |
10
|
neg_margin
|
float
|
Margin for
margin_ranking_loss_l2_squared.
OpenEA's published value is |
1.5
|
neg_margin_balance
|
float
|
Weight applied to the negative-pair loss term.
OpenEA's published value is |
0.1
|
sim_th
|
float
|
Minimum similarity for a bootstrapped pair to be accepted
-- also the bootstrapping on/off switch: OpenEA's own
|
0.0
|
bootstrap_start_epoch
|
int
|
First epoch a bootstrapping round may run,
if |
20
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/alinet.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> AliNetLinker
Train the gated multi-hop GCN/attention stack, optionally bootstrapping new pairs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
AliNetLinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/alinet.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/alinet.py
394 395 396 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/alinet.py
398 399 400 | |
AttrELinker ¶
AttrELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
literal_len: int = 5,
char_freq_threshold: float = 0.0001,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space regularized by character embeddings.
Must be fit before link can be called. See the module docstring for how this differs from IPTransELinker and JAPELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of every trained embedding table
(entities, relations, character-space entities, attributes,
characters). OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Training epochs, each one pass of structural training,
one pass of attribute/character training, and one (repeated)
pass of joint-loss training. OpenEA's config allows up to
|
500
|
batch_size
|
int
|
Mini-batch size for both the structural and attribute-triple training (each derives its own step count from its own triple count), and the divisor for the joint loss's repeat count. |
5000
|
learning_rate
|
float
|
SGD's learning rate, for every optimizer here.
OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for both the structural and attribute-triple
losses. OpenEA's published value is |
1.5
|
literal_len
|
int
|
Fixed character-sequence length each attribute value
is truncated/padded to. OpenEA's published value is |
5
|
char_freq_threshold
|
float
|
Minimum share of total (distinct-value)
character occurrences required to keep a character in the
vocabulary; rarer characters map to the padding id. OpenEA's
published value is |
0.0001
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/attre.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
attribute_triples1: list[Triple],
attribute_triples2: list[Triple],
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> AttrELinker
Train a shared TransE space jointly with a character/attribute embedding space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
attribute_triples1
|
list[Triple]
|
KG1's |
required |
attribute_triples2
|
list[Triple]
|
KG2's attribute triples. Required. |
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
AttrELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/attre.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/attre.py
454 455 456 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/attre.py
458 459 460 | |
BootEALinker ¶
BootEALinker(
embedding_dim: int = 100,
num_epochs: int = 2000,
sub_epoch: int = 10,
batch_size: int = 5000,
learning_rate: float = 0.01,
pos_margin: float = 0.01,
neg_margin: float = 2.0,
neg_margin_balance: float = 0.2,
neg_triple_num: int = 10,
truncated_epsilon: float = 0.9,
sim_th: float = 0.7,
k: int = 10,
bootstrap_pool_size: int | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space, grown by editing-based bootstrapping.
Must be fit before link can be called. See the module docstring for how this differs from MTransELinker and IPTransELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Total structural-training-epoch cap, split across
outer bootstrapping iterations of |
2000
|
sub_epoch
|
int
|
Epochs per outer iteration, between bootstrapping
rounds. OpenEA's published value is |
10
|
batch_size
|
int
|
Mini-batch size for both the structural and alignment
losses. OpenEA's published value is |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for both optimizers.
OpenEA's published value is |
0.01
|
pos_margin
|
float
|
Positive-triple margin in the "limited" loss --
positive scores aren't penalized until they exceed this.
OpenEA's published value is |
0.01
|
neg_margin
|
float
|
Negative-triple margin -- negative scores aren't
penalized until they drop below this. OpenEA's published
value is |
2.0
|
neg_margin_balance
|
float
|
Weight applied to the negative-triple loss
term relative to the positive term. OpenEA's published value
is |
0.2
|
neg_triple_num
|
int
|
Negative triples sampled per positive. OpenEA's
published value is |
10
|
truncated_epsilon
|
float
|
Fraction of each KG's entities excluded from
an entity's truncated negative-sampling candidate set (only
the top |
0.9
|
sim_th
|
float
|
Minimum similarity for a bootstrap round's candidate pair.
OpenEA's published value is |
0.7
|
k
|
int
|
Max candidate matches kept per reference-pool row before
max-weight bipartite matching. OpenEA's published value is
|
10
|
bootstrap_pool_size
|
int | None
|
Cap on each side's bootstrap reference pool
size (random subsample, fixed for the whole run) -- same
deviation-mitigation as
IPTransELinker's
own parameter: this repo's broader-than-OpenEA reference pool
(see the module docstring) means |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/bootea.py
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> BootEALinker
Train a shared TransE space with seed pseudo-triples and iterative bootstrapping.
See the module docstring for the three mechanisms that distinguish this from a plain structural baseline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
BootEALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/bootea.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/bootea.py
499 500 501 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/bootea.py
503 504 505 | |
ChatEALinker ¶
ChatEALinker(
base_linker: EmbeddingLinker,
client: LlmClient,
top_k: int = 20,
neigh_num: int = 5,
desc_neigh_num: int = 25,
desc_max_tokens: int = 80,
threshold: float = 0.5,
history_len: int = 3,
relation_names: dict[str, str] | None = None,
temporal_triples: list[TemporalTriple] | None = None,
use_name: bool = True,
use_desc: bool = True,
use_struct: bool = True,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Re-ranks an already-fitted embedding linker's top-K candidates with an LLM.
Unlike most linkers in linkingtk.algorithms.ea, has no fit() --
base_linker must already be fitted before construction (its
embeddings are what generates candidates), and the LLM itself needs
no training. This mirrors
LlmBaseLinker, which is
also fit()-free for the same reason.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_linker
|
EmbeddingLinker
|
An already-fitted linker exposing |
required |
client
|
LlmClient
|
An LlmClient (e.g. from
create_client). This
class never imports |
required |
top_k
|
int
|
Candidates considered per source entity, ranked by
|
20
|
neigh_num
|
int
|
Neighbor triples shown per entity in the reasoning
prompt (the reference's own |
5
|
desc_neigh_num
|
int
|
Neighbor triples shown per entity when
generating its one-sentence description (the reference's
own |
25
|
desc_max_tokens
|
int
|
Forwarded to the description-generation LLM call. |
80
|
threshold
|
float
|
If the base method's own top-2 candidate scores differ by more than this, accept the top-1 candidate immediately with no LLM call at all (the reference's own confidence shortcut). |
0.5
|
history_len
|
int
|
Chat turns (user+assistant pairs) kept in the running conversation history within one source entity's re-ranking. |
3
|
relation_names
|
dict[str, str] | None
|
|
None
|
temporal_triples
|
list[TemporalTriple] | None
|
Optional |
None
|
use_name
|
bool
|
Whether to score name similarity. |
True
|
use_desc
|
bool
|
Whether to generate and score entity descriptions
(requires |
True
|
use_struct
|
bool
|
Whether to score structural (neighbor-tuple) similarity. |
True
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final
links, same as every other linker's |
DEFAULT_MATCHER
|
Note
blocking is accepted for interface compliance but not used --
candidates come from base_linker's own embedding similarity,
not a BlockingStrategy, same pattern
LlmBaseLinker/
StringSimilarityLinker
already document for their own unused params. dataset2 must be
a list[Entity], not an EntitySource -- candidate generation
needs every target's embedding upfront.
Source code in src/linkingtk/algorithms/ea/chatea.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
EntMatcherLinker ¶
EntMatcherLinker()
Bases: FeatureClassifierLinker
EA linker combining hand-crafted similarity features with optimal one-to-one matching.
A preconfigured
FeatureClassifierLinker
(matching=OptimalMatcher()) — see that class for the feature set,
training, and matching behavior. Kept as its own named class since it
maps to a recognizable reference (EntMatcher's key insight: a globally
optimal assignment can outperform independent per-source matching).
Source code in src/linkingtk/algorithms/ea/entmatcher.py
27 28 | |
GCNAlignLinker ¶
GCNAlignLinker(
embedding_dim: int = 100,
num_epochs: int = 500,
learning_rate: float = 8.0,
neg_triple_num: int = 5,
gamma: float = 3.0,
min_weight: float = 0.3,
use_attributes: bool = False,
attr_dim: int = 100,
attr_top_fraction: float = 0.7,
beta: float = 0.9,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a 2-layer GCN's propagated embeddings.
Must be fit before
link can be called. See the
module docstring for what's ported vs. deliberately deviated from OpenEA,
and for the structural-only-vs.-sa (structural+attribute) tradeoff.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the propagated structural
embeddings (OpenEA's |
100
|
num_epochs
|
int
|
Training epochs. OpenEA's published value allows up to
|
500
|
learning_rate
|
float
|
Plain SGD's learning rate -- see the module docstring
for why this is unusually high by this package's conventions.
OpenEA's published value is |
8.0
|
neg_triple_num
|
int
|
Negatives sampled per seed pair per side ( |
5
|
gamma
|
float
|
Margin for
margin_ranking_loss_l1.
OpenEA's published value is |
3.0
|
min_weight
|
float
|
Floor applied to every adjacency edge weight -- see
build_weighted_adjacency.
OpenEA's own value (hardcoded, not configurable in the
reference) is |
0.3
|
use_attributes
|
bool
|
Whether to also train the attribute ( |
False
|
attr_dim
|
int
|
Dimensionality of the propagated attribute embeddings
(OpenEA's |
100
|
attr_top_fraction
|
float
|
Fraction of the combined attribute-predicate
vocabulary kept as feature columns -- see
build_attribute_features.
Only used if |
0.7
|
beta
|
float
|
Weight on the structural half of the concatenated
|
0.9
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/gcn_align.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
attribute_triples1: list[Triple] | None = None,
attribute_triples2: list[Triple] | None = None,
) -> GCNAlignLinker
Propagate structural (and, if enabled, attribute) embeddings through the graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. Unused beyond mirroring sibling
linkers' signatures -- which entities get embeddings is
determined entirely by |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
attribute_triples1
|
list[Triple] | None
|
KG1's |
None
|
attribute_triples2
|
list[Triple] | None
|
KG2's own attribute triples. See
|
None
|
Returns:
| Type | Description |
|---|---|
GCNAlignLinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/gcn_align.py
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/gcn_align.py
441 442 443 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/gcn_align.py
445 446 447 | |
IMUSELinker ¶
IMUSELinker(
embedding_dim: int = 100,
num_epochs: int = 2000,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
name_sim_threshold: float = 0.6,
entity_sim_threshold: float = 0.6,
attr_sim_threshold: float = 0.6,
top_k_attribute_pairs: int = 10,
bootstrap_iterations: int = 1,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Unsupervised EA: bootstraps its own seed pairs from attribute-value similarity.
Must be fit before
link can be called. Unlike
every other linker in this family, fit
takes no ground_truth -- see the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Training epochs, each pairing one epoch of structural
triple-loss training with one epoch of alignment-loss
training. OpenEA's config allows up to |
2000
|
batch_size
|
int
|
Mini-batch size for structural triple-loss training; the alignment-loss step count is derived from it (matching OpenEA). |
5000
|
learning_rate
|
float
|
SGD's learning rate, for both optimizers --
OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for the structural triple loss. OpenEA's
published value is |
1.5
|
name_sim_threshold
|
float
|
Minimum Levenshtein ratio for two attribute
predicates' local names to be considered aligned. OpenEA
hardcodes |
0.6
|
entity_sim_threshold
|
float
|
Minimum average Levenshtein ratio (across
shared aligned-predicate values) for two entities to be
considered aligned. OpenEA's published value
( |
0.6
|
attr_sim_threshold
|
float
|
Same as
|
0.6
|
top_k_attribute_pairs
|
int
|
Maximum aligned attribute-predicate pairs
kept after name-similarity matching, ranked by combined triple
count. OpenEA hardcodes |
10
|
bootstrap_iterations
|
int
|
Outer bootstrap rounds (entity alignment,
then optionally attribute-predicate re-alignment from those
entities). OpenEA's published default
( |
1
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/imuse.py
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
graph: Graph,
attribute_triples1: list[Triple],
attribute_triples2: list[Triple],
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> IMUSELinker
Bootstrap an alignment from attribute similarity, then train jointly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
attribute_triples1
|
list[Triple]
|
KG1's |
required |
attribute_triples2
|
list[Triple]
|
KG2's attribute triples. See
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
IMUSELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If both |
OptionalDependencyError
|
If torch or rapidfuzz isn't installed. |
Source code in src/linkingtk/algorithms/ea/imuse.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/imuse.py
434 435 436 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/imuse.py
438 439 440 | |
IPTransELinker ¶
IPTransELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
path_parm: float = 0.1,
sim_th: float = 0.7,
bootstrap_every: int = 100,
bootstrap_pool_size: int | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space, path loss, and bootstrapping.
Must be fit before link can be called. See the module docstring for how this differs from MTransELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Training epochs, each one pass over both KGs'
triples (plus path-loss minibatches). OpenEA's config allows
up to |
500
|
batch_size
|
int
|
Mini-batch size for triple-loss training, split proportionally between the two KGs by triple count each epoch (matching OpenEA). |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for both the main and
bootstrap optimizers -- OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for the triple and path losses. OpenEA's
published value is |
1.5
|
path_parm
|
float
|
Weight applied to the path (relation-composition)
loss. OpenEA's published value is |
0.1
|
sim_th
|
float
|
Minimum embedding similarity for a bootstrap round to
accept a newly found pair. OpenEA's published value is
|
0.7
|
bootstrap_every
|
int
|
Run a bootstrapping round every this many
epochs. OpenEA's published value ( |
100
|
bootstrap_pool_size
|
int | None
|
Cap on each side's bootstrap reference pool
size (random subsample, fixed for the whole run) -- see the
module docstring's deviation note. |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/iptranse.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> IPTransELinker
Train a shared TransE space with margin+path loss and periodic bootstrapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
IPTransELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/iptranse.py
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/iptranse.py
398 399 400 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/iptranse.py
402 403 404 | |
JAPELinker ¶
JAPELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
neg_alpha: float = 0.1,
top_attr_threshold: float = 0.9,
attr_sim_mat_threshold: float = 0.95,
attr_max_epoch: int = 200,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space, plus an inspectable attribute similarity.
Must be fit before
link can be called. See
the module docstring for how this differs from
MTransELinker/
IPTransELinker, and
in particular why the attribute-similarity matrix built from
attribute_triples1/attribute_triples2 no longer perturbs the
trained structural embeddings (it's exposed post-fit() as
attr_sim_mat_ instead).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained structural and
attribute embeddings. OpenEA's published EN-FR-15K-V1 config
uses |
100
|
num_epochs
|
int
|
Structural training epochs. OpenEA's config allows up
to |
500
|
batch_size
|
int
|
Mini-batch size for both the structural triple loss and (if attribute triples are given) Attr2Vec's training. |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for every optimizer used
here -- OpenEA's published value is |
0.01
|
neg_alpha
|
float
|
Weight on the negative-triple term in the structural
loss ( |
0.1
|
top_attr_threshold
|
float
|
Fraction of each KG's distinct attribute
predicates to keep, by descending frequency. OpenEA's
published value is |
0.9
|
attr_sim_mat_threshold
|
float
|
Attribute-similarity values below this are
zeroed out in the exposed |
0.95
|
attr_max_epoch
|
int
|
Attr2Vec training epochs. OpenEA's published value
is |
200
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/jape.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
attr_pool1_ids_
instance-attribute
¶
attr_pool1_ids_: list[str] = []
Entity ids indexing attr_sim_mat_'s rows.
attr_pool2_ids_
instance-attribute
¶
attr_pool2_ids_: list[str] = []
Entity ids indexing attr_sim_mat_'s columns.
attr_sim_mat_
instance-attribute
¶
attr_sim_mat_: NDArray[floating[Any]] | None = None
Thresholded attribute-similarity matrix from the last fit() call, or None if no
attribute triples were given. Rows/columns correspond to
attr_pool1_ids_/attr_pool2_ids_. Diagnostic only -- see the
module docstring for why this doesn't affect the trained
embeddings.
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
attribute_triples1: list[Triple] | None = None,
attribute_triples2: list[Triple] | None = None,
) -> JAPELinker
Train a shared TransE space; optionally build an inspectable attribute-similarity matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
attribute_triples1
|
list[Triple] | None
|
KG1's |
None
|
attribute_triples2
|
list[Triple] | None
|
KG2's attribute triples. See
|
None
|
Returns:
| Type | Description |
|---|---|
JAPELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/jape.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/jape.py
468 469 470 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/jape.py
472 473 474 | |
KDCoELinker ¶
KDCoELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
alpha: float = 5.0,
new_param: float = 0.1,
wv_dim: int = 300,
default_desc_length: int = 4,
desc_batch_size: int = 512,
desc_sim_th: float = 0.95,
sim_th: float = 0.8,
max_co_training_iters: int = 5,
word_embed_url: str = _FASTTEXT_URL,
cache_dir: Path | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Co-trains a structural TransE+mapping space with a description-text encoder.
Must be fit before link can be called. See the module docstring for how this differs from MTransELinker and IPTransELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings and the (square) mapping matrix. OpenEA's published
EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Per-phase training-epoch cap (structural or
description), each co-training iteration. OpenEA's config
allows up to |
500
|
batch_size
|
int
|
Mini-batch size for structural triple-loss training; the mapping-loss batch size is derived from it (same number of minibatches per epoch for both, matching OpenEA). |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for every optimizer here
(structural, mapping, mapping-new, description) -- OpenEA's
published value is |
0.01
|
margin
|
float
|
Hinge margin for the structural triple loss. OpenEA's
published value is |
1.5
|
alpha
|
float
|
Weight applied to the seed-pairs mapping loss. OpenEA's
published value is |
5.0
|
new_param
|
float
|
Weight applied to the bootstrapped-pairs mapping loss.
OpenEA's published value is |
0.1
|
wv_dim
|
int
|
Pretrained word-vector dimensionality. Must match
|
300
|
default_desc_length
|
int
|
Fixed word-sequence length each entity's
description is padded/truncated to. OpenEA's published value
is |
4
|
desc_batch_size
|
int
|
Mini-batch size for description-encoder training.
OpenEA's published value is |
512
|
desc_sim_th
|
float
|
Kept for interface parity with OpenEA's published
value ( |
0.95
|
sim_th
|
float
|
Minimum (mapped) structural-embedding similarity for a
bootstrap round to accept a newly found pair. OpenEA's
published value is |
0.8
|
max_co_training_iters
|
int
|
Cap on outer co-training iterations (each
alternating a description-encoder phase and a structural
phase). OpenEA's published value ( |
5
|
word_embed_url
|
str
|
URL of a fastText-format |
_FASTTEXT_URL
|
cache_dir
|
Path | None
|
Passed through to |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/kdcoe.py
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
attribute_triples1: list[Triple] | None = None,
attribute_triples2: list[Triple] | None = None,
) -> KDCoELinker
Co-train the structural+mapping space and the description encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping in both phases -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
attribute_triples1
|
list[Triple] | None
|
KG1's |
None
|
attribute_triples2
|
list[Triple] | None
|
KG2's attribute triples. See
|
None
|
Returns:
| Type | Description |
|---|---|
KDCoELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/kdcoe.py
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side (projected).
Source code in src/linkingtk/algorithms/ea/kdcoe.py
715 716 717 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side (unprojected).
Source code in src/linkingtk/algorithms/ea/kdcoe.py
719 720 721 | |
KGELinker ¶
KGELinker(
model: str = "TransE",
embedding_dim: int = 50,
num_epochs: int = 50,
batch_size: int | None = None,
learning_rate: float = 0.01,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Scores candidate pairs by trained knowledge-graph-embedding similarity.
Must be fit before
link can be called. Unlike
FeatureClassifierLinker,
training needs the KGs' relational structure, not just entity text --
see fit's graph argument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
A model name from pykeen's registry
( |
'TransE'
|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation embeddings. |
50
|
num_epochs
|
int
|
Training epochs. |
50
|
batch_size
|
int | None
|
Training batch size. Defaults to pykeen's own
auto-selection ( |
None
|
learning_rate
|
float
|
Adam's learning rate. pykeen's own default
( |
0.01
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. OptimalMatcher's dense Hungarian-algorithm cost matrix is explicitly not meant for KG-scale entity counts (see its docstring) -- avoid it here for anything but small graphs. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/ea/kge.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
) -> KGELinker
Train KGE embeddings jointly over graph plus seed alignment triples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. |
required |
dataset2
|
list[Entity]
|
Target entities. |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, pykeen trains non-deterministically. |
None
|
Returns:
| Type | Description |
|---|---|
KGELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If no |
OptionalDependencyError
|
If pykeen isn't installed. |
Source code in src/linkingtk/algorithms/ea/kge.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/kge.py
220 221 222 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/kge.py
224 225 226 | |
MTransELinker ¶
MTransELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
alpha: float = 5.0,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a learned cross-KG mapping over shared TransE embeddings.
Must be fit before link can be called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings and the (square) mapping matrix. OpenEA's published
EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Training epochs, each alternating one epoch of
triple-loss training with one epoch of mapping-loss training.
OpenEA's config allows up to |
500
|
batch_size
|
int
|
Mini-batch size for triple-loss training; the per-epoch mapping-loss batch size is derived from it (matching OpenEA: the same number of mini-batches per epoch for both). |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for both the embedding and
mapping optimizers -- OpenEA's published value is |
0.01
|
alpha
|
float
|
Weight applied to the mapping loss (approximation error
plus orthogonality regularization). OpenEA's published value
is |
5.0
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/mtranse.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> MTransELinker
Train shared TransE embeddings plus a learned cross-KG mapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. Unused beyond mirroring
KGELinker.fit's
signature -- which entities get embeddings is determined
entirely by |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
MTransELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/mtranse.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side (projected).
Source code in src/linkingtk/algorithms/ea/mtranse.py
301 302 303 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side (unprojected).
Source code in src/linkingtk/algorithms/ea/mtranse.py
305 306 307 | |
MultiKELinker ¶
MultiKELinker(
embedding_dim: int = 100,
num_epochs: int = 2000,
batch_size: int = 5000,
entity_batch_size: int = 5000,
attribute_batch_size: int = 5000,
learning_rate: float = 0.001,
common_space_learning_rate: float = 0.004,
neg_triple_num: int = 10,
cv_weight: float = 1.0,
predicate_init_sim: float = 0.9,
predicate_soft_sim: float = 0.8,
start_predicate_soft_alignment: int = 10,
min_predicate_triple_count: int = 10,
literal_encoder_model: str = "distilbert-base-multilingual-cased",
literal_max_length: int = 16,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared embedding table unifying name/relation/attribute views.
Must be fit before link can be called. See the module docstring for the full architecture and the three documented scope cuts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of every trained embedding table.
OpenEA's published EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Training epochs, each one pass of relation-view,
common-space, cross-KG relation-entity (and, once warmed up,
cross-KG relation-predicate) training, then the same four
steps for the attribute view. OpenEA's config allows up to
|
2000
|
batch_size
|
int
|
Mini-batch size for the relation view's main loss and its two cross-KG inference terms. |
5000
|
entity_batch_size
|
int
|
Mini-batch size for common-space learning. |
5000
|
attribute_batch_size
|
int
|
Mini-batch size for the attribute view's main loss and its two cross-KG inference terms. |
5000
|
learning_rate
|
float
|
SGD's learning rate for every optimizer except
common-space learning. OpenEA's published value is |
0.001
|
common_space_learning_rate
|
float
|
SGD's learning rate for common-space
learning specifically (OpenEA's own |
0.004
|
neg_triple_num
|
int
|
Negatives sampled per positive relation triple --
only the main relation-view loss uses negative sampling at
all (see the module docstring). Published value is |
10
|
cv_weight
|
float
|
Weight applied to the common-space-learning loss.
Published value is |
1.0
|
predicate_init_sim
|
float
|
Levenshtein-ratio threshold (of humanized
local names) for the one-time mutual-best-match predicate
alignment. Published value is |
0.9
|
predicate_soft_sim
|
float
|
Similarity floor used to rescale an aligned
predicate pair's weight ( |
0.8
|
start_predicate_soft_alignment
|
int
|
Epoch (0-indexed) at which the two
predicate-alignment cross-KG loss terms start contributing --
a warm-up gate. Published value is |
10
|
min_predicate_triple_count
|
int
|
Attribute predicates with fewer than
this many combined (both-KG) triples are dropped before
anything else. Published value is |
10
|
literal_encoder_model
|
str
|
Hugging Face model id for the literal
encoder (see the module docstring's "The literal encoder"
section). Defaults to |
'distilbert-base-multilingual-cased'
|
literal_max_length
|
int
|
Token-truncation length passed to the literal encoder for every name/value string. |
16
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/multike.py
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
attribute_triples1: list[Triple],
attribute_triples2: list[Triple],
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> MultiKELinker
Train the three-view embedding and a shared space unifying them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
attribute_triples1
|
list[Triple]
|
KG1's |
required |
attribute_triples2
|
list[Triple]
|
KG2's attribute triples. Required. |
required |
random_state
|
int | None
|
Seed for reproducible training (also seeds the literal encoder's random projection). Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
MultiKELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/multike.py
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/multike.py
752 753 754 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/multike.py
756 757 758 | |
RDGCNLinker ¶
RDGCNLinker(
embedding_dim: int = 300,
num_epochs: int = 500,
learning_rate: float = 0.002,
neg_triple_num: int = 125,
gamma: float = 1.0,
alpha: float = 0.1,
beta: float = 0.3,
default_name_length: int = 4,
word_embed_url: str = _FASTTEXT_URL,
cache_dir: Path | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via RDGCN's dual/primal-attention-plus-GCN embeddings.
Must be fit before
link can be called. See
the module docstring for what's ported vs. deliberately deviated from
OpenEA, and why dataset1/dataset2 must come from
EnFr15KAttrDataset rather than
EnFr15KDataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Entity-embedding dimensionality -- must equal the
pretrained word vectors' own dimensionality (no projection
layer sits between them). OpenEA's published EN-FR-15K-V1
config uses |
300
|
num_epochs
|
int
|
Training epochs. OpenEA's config allows up to |
500
|
learning_rate
|
float
|
Adam's learning rate. OpenEA's published value is
|
0.002
|
neg_triple_num
|
int
|
Hard negatives mined per seed pair per side ( |
125
|
gamma
|
float
|
Margin for
margin_ranking_loss_l1.
OpenEA's published value is |
1.0
|
alpha
|
float
|
Round-1 primal-embedding mixing weight. OpenEA's published
value is |
0.1
|
beta
|
float
|
Round-2 primal-embedding mixing weight. OpenEA's published
value is |
0.3
|
default_name_length
|
int
|
Max local-name tokens summed per entity for
the initial embedding. OpenEA's published value is |
4
|
word_embed_url
|
str
|
URL of a fastText-format |
_FASTTEXT_URL
|
cache_dir
|
Path | None
|
Passed through to |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/rdgcn.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> RDGCNLinker
Train the two-round dual/primal-attention GCN over the combined graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- must carry non-empty |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
RDGCNLinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If no entity in |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/rdgcn.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/rdgcn.py
417 418 419 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/rdgcn.py
421 422 423 | |
RSN4EALinker ¶
RSN4EALinker(
hidden_size: int = 100,
num_layers: int = 2,
keep_prob: float = 0.6,
max_length: int = 15,
alpha: float = 0.7,
beta: float = 0.7,
repeat_times: int = 2,
max_paths: int | None = None,
num_epochs: int = 30,
batch_size: int = 512,
learning_rate: float = 0.0005,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a Recurrent Skipping Network over sampled walk paths.
Must be fit before link can be called. Unlike MTransELinker/ SEALinker, scoring uses raw (unprojected) embeddings on both sides -- RSN4EA never builds a mapping matrix; see the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hidden_size
|
int
|
Entity/relation embedding dimensionality and LSTM
hidden size. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_layers
|
int
|
LSTM depth. OpenEA's published value is |
2
|
keep_prob
|
float
|
Per-LSTM-layer output dropout keep-probability. OpenEA's
published value is |
0.6
|
max_length
|
int
|
Sampled path length -- must be odd and >= 3 (entities
at even positions, relations at odd positions). OpenEA's
published value is |
15
|
alpha
|
float
|
Path-sampling depth-bias weight (down-weights continuations
already directly reachable from a walk's own start entity).
OpenEA's published value is |
0.7
|
beta
|
float
|
Path-sampling cross-KG bias weight (up-weights continuations
whose tail is a seed-pair entity). OpenEA's published value is
|
0.7
|
repeat_times
|
int
|
Walks sampled per row of the augmented knowledge
base. OpenEA's published value is |
2
|
max_paths
|
int | None
|
Optional cap on the number of sampled paths kept for
training (random subsample, fixed for the run) -- see the
module docstring. |
None
|
num_epochs
|
int
|
Training epochs. OpenEA's published value is |
30
|
batch_size
|
int
|
Mini-batch size for path training. OpenEA's published
value is |
512
|
learning_rate
|
float
|
Adam's learning rate (see the module docstring for
why Adam, not the config's stated Adagrad). OpenEA's published
value is |
0.0005
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> RSN4EALinker
Sample walk paths over an alias-expanded graph and train the RSN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. Unused beyond mirroring sibling
linkers' signatures -- which entities get embeddings is
determined entirely by |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity
ids on both sides must already be disjoint, as they are
from a
GraphDatasetLoader's
|
required |
random_state
|
int | None
|
Seed for reproducible path sampling and training. Left unspecified, both are non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
RSN4EALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
373 374 375 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
377 378 379 | |
SEALinker ¶
SEALinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
alpha_1: float = 2.5,
alpha_2: float = 0.25,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via structural TransE plus a semi-supervised dual mapping.
Must be fit before link can be called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings and both (square) mapping matrices. OpenEA's
published EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Training epochs, each alternating one epoch of
structural triple-loss training with one epoch of mapping-loss
training. OpenEA's config allows up to |
500
|
batch_size
|
int
|
Mini-batch size for structural triple-loss training, split proportionally between the two KGs by triple count each epoch. The mapping loss reuses the resulting step count. |
5000
|
learning_rate
|
float
|
Adam's learning rate, for both the structural and
mapping optimizers -- OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for the structural triple loss. OpenEA's
published value is |
1.5
|
alpha_1
|
float
|
Weight applied to the supervised mapping loss (seed-pair
approximation error). OpenEA's published value is |
2.5
|
alpha_2
|
float
|
Weight applied to the semi-supervised (cycle-consistency)
mapping loss over unlabeled entities. OpenEA's published value
is |
0.25
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/sea.py
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> SEALinker
Train shared structural TransE embeddings plus a dual cross-KG mapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
SEALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/sea.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Projected via mapping_mat_1.
Source code in src/linkingtk/algorithms/ea/sea.py
406 407 408 409 410 411 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side (unprojected).
Source code in src/linkingtk/algorithms/ea/sea.py
413 414 415 | |
SimpleHHEALinker ¶
SimpleHHEALinker(
name_model: str = "albert-base-v2",
emb_size: int = 64,
structure_size: int = 8,
time_size: int = 8,
use_structure: bool = True,
walk_length: int = 80,
num_walks: int = 10,
structure_dim: int = 64,
max_degree: int = 1000,
structure_workers: int = 1,
structure_epochs: int = 5,
num_epochs: int = 1500,
learning_rate: float = 0.01,
weight_decay: float = 0.001,
gamma: float = 1.0,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Fuses whitened name embeddings, node2vec structural embeddings, and (optionally) Time2Vec temporal embeddings for Entity Alignment.
Must be fit
before link. Exposes source_embedding/target_embedding like
KGELinker, so it's a drop-in
for rank_exhaustive -- and,
once #22 (ChatEA) lands, a candidate base_linker for it too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name_model
|
str
|
Hugging Face model id for name embedding, loaded via
|
'albert-base-v2'
|
emb_size
|
int
|
Final fused embedding dimension. |
64
|
structure_size
|
int
|
Post-projection size of the structure branch before concatenation into the final embedding. |
8
|
time_size
|
int
|
Post-projection size of the time branch before concatenation into the final embedding. |
8
|
use_structure
|
bool
|
Whether to compute and use node2vec structural
embeddings at all (the reference's own |
True
|
walk_length
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
80
|
num_walks
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
10
|
structure_dim
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
64
|
max_degree
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
1000
|
structure_workers
|
int
|
Worker threads for the structural Word2Vec fit -- see build_structure_embeddings. Walk simulation itself is always single-threaded, matching the reference's own default invocation. |
1
|
structure_epochs
|
int
|
Word2Vec training epochs for the structural skip-gram model -- see build_structure_embeddings. |
5
|
num_epochs
|
int
|
Training hyperparameter -- see train. |
1500
|
learning_rate
|
float
|
Training hyperparameter -- see train. |
0.01
|
weight_decay
|
float
|
Training hyperparameter -- see train. |
0.001
|
gamma
|
float
|
Training hyperparameter -- see train. |
1.0
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train/embed on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
temporal_triples: list[TemporalTriple] | None = None,
random_state: int | None = None,
) -> SimpleHHEALinker
Trains the fusion model over dataset1/dataset2's entities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. |
required |
dataset2
|
list[Entity]
|
Target entities. |
required |
ground_truth
|
list[tuple[str, str]]
|
Known-correct |
required |
graph
|
Graph
|
Combined relational structure of both KGs (e.g.
|
required |
temporal_triples
|
list[TemporalTriple] | None
|
Optional |
None
|
random_state
|
int | None
|
Seed for reproducible training. |
None
|
Returns:
| Type | Description |
|---|---|
SimpleHHEALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If no |
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
285 286 287 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
289 290 291 | |
entmatcher ¶
EntMatcher-style Entity Alignment linker.
References
EntMatcher: An Open-Source Library for Entity Alignment via Feature Matching in Knowledge Graphs. https://github.com/DexterZeng/EntMatcher (research repo, not a dependency of this project — see linkingtk.algorithms.feature_classifier for what's reused).
EntMatcherLinker ¶
EntMatcherLinker()
Bases: FeatureClassifierLinker
EA linker combining hand-crafted similarity features with optimal one-to-one matching.
A preconfigured
FeatureClassifierLinker
(matching=OptimalMatcher()) — see that class for the feature set,
training, and matching behavior. Kept as its own named class since it
maps to a recognizable reference (EntMatcher's key insight: a globally
optimal assignment can outperform independent per-source matching).
Source code in src/linkingtk/algorithms/ea/entmatcher.py
27 28 | |
Knowledge Graph Embeddings¶
kge ¶
Knowledge-graph-embedding linker for Entity Alignment.
Wraps pykeen (https://github.com/pykeen/pykeen) translational and bilinear
embedding models -- TransE, DistMult, RotatE by default -- to score candidate pairs by
embedding similarity rather than string/feature similarity. pykeen has no
built-in Entity Alignment mode (it's a link-prediction library); the
approach here is a simplified version of the MTransE/IPTransE family's
core idea, not a reimplementation of either paper's full method (no
separate per-KG embedding spaces plus a learned transform, no triple
-swapping): both KGs' triples are combined into a single graph, the known
training alignment pairs are added to it as extra (source_id,
"__seed_alignment__", target_id) triples, and a single KGE model is
trained jointly over all of it -- pulling aligned (and structurally
analogous) entities' embeddings together via that one pseudo-relation.
KGELinker ¶
KGELinker(
model: str = "TransE",
embedding_dim: int = 50,
num_epochs: int = 50,
batch_size: int | None = None,
learning_rate: float = 0.01,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Scores candidate pairs by trained knowledge-graph-embedding similarity.
Must be fit before
link can be called. Unlike
FeatureClassifierLinker,
training needs the KGs' relational structure, not just entity text --
see fit's graph argument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
A model name from pykeen's registry
( |
'TransE'
|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation embeddings. |
50
|
num_epochs
|
int
|
Training epochs. |
50
|
batch_size
|
int | None
|
Training batch size. Defaults to pykeen's own
auto-selection ( |
None
|
learning_rate
|
float
|
Adam's learning rate. pykeen's own default
( |
0.01
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. OptimalMatcher's dense Hungarian-algorithm cost matrix is explicitly not meant for KG-scale entity counts (see its docstring) -- avoid it here for anything but small graphs. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/ea/kge.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
) -> KGELinker
Train KGE embeddings jointly over graph plus seed alignment triples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. |
required |
dataset2
|
list[Entity]
|
Target entities. |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, pykeen trains non-deterministically. |
None
|
Returns:
| Type | Description |
|---|---|
KGELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If no |
OptionalDependencyError
|
If pykeen isn't installed. |
Source code in src/linkingtk/algorithms/ea/kge.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/kge.py
220 221 222 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/kge.py
224 225 226 | |
MTransE¶
mtranse ¶
MTransE knowledge-graph-embedding linker for Entity Alignment.
Chen, M., Tian, Y., Yang, M., & Zaniolo, C. (2017). Multilingual Knowledge Graph Embeddings for Cross-lingual Knowledge Alignment. IJCAI 2017. https://www.ijcai.org/proceedings/2017/0209.pdf
Unlike KGELinker's simplified single-shared-pseudo-relation trick, this is a faithful reimplementation of MTransE's actual training procedure, ported directly from OpenEA's reference implementation (https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/mtranse.py and .../modules/base/mapping.py) rather than from a from-scratch reading of the paper, since OpenEA's actual algorithm differs from a naive paper reading in ways that matter for reproducing its published numbers:
- Entities and relations from both KGs share one embedding table (not two independently-trained per-KG spaces). Since each KG's own triples only ever reference that KG's own entities, this has the same practical effect as two separate spaces, but reuses this repo's existing combined-graph id-mapping utilities directly.
- The embedding step uses pure positive-triple loss (
sum(||h+r-t||^2)over real triples only) with no negative sampling -- unlikeKGELinker/pykeen'sSLCWATrainingLoop. This is why training is hand-rolled in PyTorch here rather than routed through pykeen. - Entities and relations are L2-normalized to the unit sphere on every forward pass (not just at initialization) -- this is what keeps the no-negative-sampling loss from collapsing to the trivial zero solution.
- Alignment signal comes from a learned square mapping matrix (orthogonally
initialized, trained against a soft-orthogonality-regularized loss over
seed pairs), not from injecting pseudo-triples into the embedding space
itself. Critically, the mapping loss's optimizer updates both the
mapping matrix and the shared entity embeddings themselves -- OpenEA's
generate_optimizer(loss, lr, var_list=None, ...)leavesvar_listunset, which makes TensorFlow'scompute_gradientsdifferentiate against every trainable variable connected to the loss, not just the mapping matrix. Restricting this port's mapping optimizer to only the mapping matrix (the more "obvious" reading of the algorithm) silently drops that second training signal and leaves the mapping loss converging roughly an order of magnitude slower.
MTransELinker ¶
MTransELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
alpha: float = 5.0,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a learned cross-KG mapping over shared TransE embeddings.
Must be fit before link can be called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings and the (square) mapping matrix. OpenEA's published
EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Training epochs, each alternating one epoch of
triple-loss training with one epoch of mapping-loss training.
OpenEA's config allows up to |
500
|
batch_size
|
int
|
Mini-batch size for triple-loss training; the per-epoch mapping-loss batch size is derived from it (matching OpenEA: the same number of mini-batches per epoch for both). |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for both the embedding and
mapping optimizers -- OpenEA's published value is |
0.01
|
alpha
|
float
|
Weight applied to the mapping loss (approximation error
plus orthogonality regularization). OpenEA's published value
is |
5.0
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/mtranse.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> MTransELinker
Train shared TransE embeddings plus a learned cross-KG mapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. Unused beyond mirroring
KGELinker.fit's
signature -- which entities get embeddings is determined
entirely by |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
MTransELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/mtranse.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side (projected).
Source code in src/linkingtk/algorithms/ea/mtranse.py
301 302 303 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side (unprojected).
Source code in src/linkingtk/algorithms/ea/mtranse.py
305 306 307 | |
IPTransE¶
iptranse ¶
IPTransE knowledge-graph-embedding linker for Entity Alignment.
Zhu, H., Xu, R., Yang, Y., Lin, X., & Cheng, X. (2017). Iterative Entity Alignment via Joint Knowledge Embeddings. IJCAI 2017. https://www.ijcai.org/proceedings/2017/0595.pdf
Like MTransELinker, this is
a faithful reimplementation of IPTransE's actual training procedure, ported
directly from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/iptranse.py
and its modules/bootstrapping, modules/train/batch.py,
modules/load/read.py support code) rather than from a from-scratch
reading of the paper. Three things distinguish it from MTransE:
- Seed pairs share one embedding row, not two rows plus a learned
mapping (OpenEA's
alignment_module="sharing"): a seed pair's target entity is aliased to its source entity's id at training-tensor-build time. Alignment signal for held-out entities comes entirely from this shared space plus the two mechanisms below, not from any post-hoc projection. - Training loss is margin-based (
margin=1.5, one negative per positive, corrupting the head or tail from that triple's own KG), not MTransE's negative-sampling-free positive-only loss -- plus a relation-composition ("path") loss over 2-hop chains within each KG, weighted bypath_parm, that encourages composed relations (r_x + r_y) to approximate a direct edge (r) when both exist. - Iterative bootstrapping: every
bootstrap_everyepochs, an unsupervised self-training round computes embedding similarity between every not-yet-seeded entity on each side, keeps each source entity's best match if it clearssim_th, and turns each newly-found pair's real edges into weighted pseudo-triples trained for one extra epoch via a separate optimizer -- this is what "iterative" refers to in the method's name.
Ported into two private helper modules: _iptranse_training.py (plain
numpy, independently testable without torch) and _iptranse_torch.py
(the training-step functions that build/consume PyTorch tensors).
Deviations from OpenEA's own code, beyond the negative-sampling/path-generation simplifications documented in those two modules:
- OpenEA partitions relation triples into two KGs via its own
KGsloader; this repo's EA linker family instead takes one combinedgraphinfit(). IPTransE genuinely needs the KG1/KG2 boundary (per-KG negative sampling, path generation, pseudo-triple dicts), so it's re-derived here fromdataset1/dataset2's own id sets: a triple belongs to KG1 iff its head id is one ofdataset1's ids. This relies on the same assumptionMTransELinkeralready documents (ids are disjoint across the two KGs) for a new purpose. - OpenEA's bootstrap reference pool is
valid_entities + test_entitiesfrom its own loader (entities only, no labels needed). This repo'sfit()only receivesdataset1/dataset2(full entity rosters) andground_truth(the train split) -- so the reference pool here is everydataset1/dataset2entity not already aground_truthsource/target, which is broader than OpenEA's val+test-only pool. At real dataset scale this is a dense similarity matrix over most of both KGs' entities;bootstrap_pool_sizelets callers cap it (random subsample, fixed for the whole run) if that's a problem.
IPTransELinker ¶
IPTransELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
path_parm: float = 0.1,
sim_th: float = 0.7,
bootstrap_every: int = 100,
bootstrap_pool_size: int | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space, path loss, and bootstrapping.
Must be fit before link can be called. See the module docstring for how this differs from MTransELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Training epochs, each one pass over both KGs'
triples (plus path-loss minibatches). OpenEA's config allows
up to |
500
|
batch_size
|
int
|
Mini-batch size for triple-loss training, split proportionally between the two KGs by triple count each epoch (matching OpenEA). |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for both the main and
bootstrap optimizers -- OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for the triple and path losses. OpenEA's
published value is |
1.5
|
path_parm
|
float
|
Weight applied to the path (relation-composition)
loss. OpenEA's published value is |
0.1
|
sim_th
|
float
|
Minimum embedding similarity for a bootstrap round to
accept a newly found pair. OpenEA's published value is
|
0.7
|
bootstrap_every
|
int
|
Run a bootstrapping round every this many
epochs. OpenEA's published value ( |
100
|
bootstrap_pool_size
|
int | None
|
Cap on each side's bootstrap reference pool
size (random subsample, fixed for the whole run) -- see the
module docstring's deviation note. |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/iptranse.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> IPTransELinker
Train a shared TransE space with margin+path loss and periodic bootstrapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
IPTransELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/iptranse.py
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/iptranse.py
398 399 400 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/iptranse.py
402 403 404 | |
_iptranse_training ¶
Private training helpers for IPTransELinker.
Pure, independently-testable functions ported from OpenEA's reference
implementation (https://github.com/nju-websoft/OpenEA --
approaches/iptranse.py, modules/bootstrapping/alignment_finder.py,
modules/train/batch.py, modules/load/read.py). Not part of the
public API -- see iptranse.py for the orchestrating class and the
deviations from OpenEA's own code documented in its docstring.
build_shared_id_mappings ¶
build_shared_id_mappings(
triples: list[Triple], seed_pairs: list[tuple[str, str]]
) -> tuple[dict[str, int], dict[str, int]]
Entity/relation id mappings with IPTransE's "sharing" alignment semantics.
Every seed pair's target entity is aliased to its source entity's id --
both get exactly one embedding row, not two independently-trained rows
linked by a learned mapping (contrast
MTransELinker).
Relations are not merged across KGs (mirrors OpenEA's
generate_sharing_id([], ...) fallthrough for relations -- the same
plain sorted-union behavior
build_id_mappings already
implements, since real relation label strings never coincide across
the two KGs in these datasets).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
list[Triple]
|
Combined triples from both KGs. |
required |
seed_pairs
|
list[tuple[str, str]]
|
|
required |
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
|
dict[str, int]
|
exactly |
tuple[dict[str, int], dict[str, int]]
|
values, matching OpenEA's |
Source code in src/linkingtk/algorithms/ea/_iptranse_training.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
find_mutual_pairs ¶
find_mutual_pairs(
sim_mat: NDArray[floating[Any]], sim_th: float = 0.0
) -> list[tuple[int, int, float]]
Reciprocal top-1 match finder for bootstrapping under saturated similarities.
Keeps (i, j) only if j is row i's best match and i is
column j's best match (both argmaxes agree), unlike
find_new_pairs's
one-sided row-argmax. Added for KDCoE's description pathway (#29): once a
similarity matrix saturates near 1.0 for nearly every entry (as the
description encoder's cosine similarities do on real EN-FR-15K-V1 --
median ~0.9998), an absolute threshold like desc_sim_th can't
discriminate anything -- almost every row clears it. Reciprocity stays
useful under saturation because the argmax positions (which column is
largest per row, which row is largest per column) still vary even when
the values themselves are nearly indistinguishable, and requiring both
sides to agree rejects most of the one-sided noise a lone threshold
can't. sim_th is kept only as a coarse sanity floor (default 0.0,
i.e. no real filtering) -- reciprocity is the operative filter here, not
the threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sim_mat
|
NDArray[floating[Any]]
|
|
required |
sim_th
|
float
|
Minimum similarity for a reciprocal match to count. Defaults
to |
0.0
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int, float]]
|
|
list[tuple[int, int, float]]
|
best-match pair clearing |
Source code in src/linkingtk/algorithms/ea/_iptranse_training.py
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
find_new_pairs ¶
find_new_pairs(
sim_mat: NDArray[floating[Any]], sim_th: float
) -> list[tuple[int, int, float]]
Row-wise top-1 + threshold match finder for bootstrapping.
For each row i, keeps (i, argmax_j sim[i, j]) iff
sim[i, argmax_j] > sim_th. Reimplements OpenEA's
find_potential_alignment_greedily (=
find_alignment(sim_mat, sim_th, k=1)) directly with numpy -- with
k=1, intersecting "threshold-filtered pairs" with "each row's
top-1 neighbor" reduces exactly to this row-wise argmax+threshold
check, so there's no need to port filter_sim_mat/
search_nearest_k's two-pass set-intersection, or the unused (by
IPTransE) igraph/graph-tool-based find_potential_alignment_mwgm
variant.
This is not mutual/bipartite matching -- two different rows can map to the same column.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sim_mat
|
NDArray[floating[Any]]
|
|
required |
sim_th
|
float
|
Minimum similarity for a row's best match to count. |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, int, float]]
|
|
list[tuple[int, int, float]]
|
best match clears |
Source code in src/linkingtk/algorithms/ea/_iptranse_training.py
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
generate_two_step_paths ¶
generate_two_step_paths(
triples: list[Triple],
) -> list[tuple[str, str, str, int]]
2-hop relation-composition paths within one KG's own triples.
For every chain h_x --r_x--> m --r_y--> t_y where a direct edge
h_x --r--> t_y also exists, emits (r_x, r_y, r, path_weight)
where path_weight = fanout(h_x, r_x) * fanout(m, r_y) -- a proxy
for how generic/common this 2-hop composition is. Chains with
path_weight >= 101 are dropped (OpenEA's own hub-node cutoff).
Mirrors OpenEA's generate_2steps_path
(approaches/iptranse.py), reimplemented with dict grouping instead
of a pandas groupby/self-join/inner-join pipeline -- this repo has no
pandas dependency and none should be added for this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
list[Triple]
|
One KG's own triples (not combined across KGs -- paths are meaningless across disjoint entity spaces). |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[str, str, str, int]]
|
|
list[tuple[str, str, str, int]]
|
unmapped to ids. |
Source code in src/linkingtk/algorithms/ea/_iptranse_training.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
pseudo_triples_for_pair ¶
pseudo_triples_for_pair(
entity_a: int,
entity_b: int,
weight: float,
by_head: dict[int, list[tuple[int, int]]],
by_tail: dict[int, list[tuple[int, int]]],
) -> set[tuple[int, int, int, float]]
Weighted pseudo-triples generated from a newly bootstrapped pair.
If entity_a and entity_b are plausibly the same real-world
entity, each of entity_a's edges plausibly also holds with
entity_b substituted in. by_head/by_tail must be
entity_a's own KG's edge dicts; call this twice per pair (once
per direction, with by_head/by_tail swapped for the other
KG's dicts and the arguments swapped) to cover both entities' edges,
per OpenEA's generate_triples_of_latent_ents.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_a
|
int
|
The entity whose real edges are being copied over. |
required |
entity_b
|
int
|
The (bootstrapped) counterpart substituted into those edges. |
required |
weight
|
float
|
The pair's similarity score, carried through as the pseudo-triple's training weight. |
required |
by_head
|
dict[int, list[tuple[int, int]]]
|
|
required |
by_tail
|
dict[int, list[tuple[int, int]]]
|
|
required |
Returns:
| Type | Description |
|---|---|
set[tuple[int, int, int, float]]
|
|
set[tuple[int, int, int, float]]
|
|
Source code in src/linkingtk/algorithms/ea/_iptranse_training.py
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | |
sample_negative_path_relations ¶
sample_negative_path_relations(
positive_paths: NDArray[int64],
relation_pool: NDArray[int64],
rng: Generator,
) -> npt.NDArray[np.int64]
Corrupt a path batch's composed relation for the path loss's negatives.
Replaces column 2 (r, the composed/direct relation) with a
uniformly random relation from relation_pool -- no real-triple
exclusion check, mirroring OpenEA's generate_neg_paths, which has
none either.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positive_paths
|
NDArray[int64]
|
|
required |
relation_pool
|
NDArray[int64]
|
That KG's own relation ids to sample replacements from. |
required |
rng
|
Generator
|
Random generator, for reproducibility. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[int64]
|
column 2 replaced. |
Source code in src/linkingtk/algorithms/ea/_iptranse_training.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
sample_negative_triples ¶
sample_negative_triples(
positive_triples: NDArray[int64],
entity_pool: NDArray[int64],
real_triples: set[tuple[int, int, int]],
rng: Generator,
max_tries: int = 10,
) -> npt.NDArray[np.int64]
Corrupt one endpoint of each positive triple to build a negative.
For each row, a fair coin decides whether the head or tail is
corrupted; the replacement is drawn uniformly from entity_pool
(that triple's own KG's entities for the main triple loss, or the
combined KG1+KG2 pool for the bootstrap/alignment loss -- see
callers). Best-effort avoids reproducing a real triple: retries up to
max_tries, then accepts the last draw regardless.
Deliberately simplified from OpenEA's vectorized
generate_neg_triples_fast to a plain per-row retry loop, for
readability -- same statistical behavior, same precedent as
mtranse.py's _validation_hits1 simplification.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positive_triples
|
NDArray[int64]
|
|
required |
entity_pool
|
NDArray[int64]
|
Candidate replacement entity ids. |
required |
real_triples
|
set[tuple[int, int, int]]
|
Set of |
required |
rng
|
Generator
|
Random generator, for reproducibility. |
required |
max_tries
|
int
|
Retries per row before giving up and accepting the last (possibly still-real) draw. |
10
|
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[int64]
|
|
Source code in src/linkingtk/algorithms/ea/_iptranse_training.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
JAPE¶
jape ¶
JAPE knowledge-graph-embedding linker for Entity Alignment.
Sun, Z., Hu, W., & Li, C. (2017). Cross-Lingual Entity Alignment via Joint Attribute-Preserving Embedding. ISWC 2017. https://link.springer.com/chapter/10.1007/978-3-319-68288-4_37
Like MTransELinker and IPTransELinker, this is a faithful reimplementation of JAPE's actual training procedure, ported directly from OpenEA's reference implementation (https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/jape.py and .../attr2vec.py) rather than from a from-scratch reading of the paper. JAPE combines two signals:
- Structural embedding: the same shared-id ("sharing") mechanism as
IPTransELinker-- reuses_iptranse_training.py'sbuild_shared_id_mappingsandsample_negative_triplesdirectly -- trained viasum(||h+r-t||^2) - neg_alpha * sum(||h'+r-t'||^2). Unlike MTransE/IPTransE, this has no margin/hinge -- it's OpenEA's actual (unbounded-below) formula for JAPE specifically, not a simplification. - Attribute-correlation embedding: a skip-gram-style model (ported from
OpenEA's
Attr2Vec) trained over which attribute predicates co-occur on the same entity -- entities that are a known seed pair have their attribute vocabularies merged before training, which is what makes this cross-lingual-aware. The trained attribute embeddings produce a per-entity attribute vector (mean-pooled, L2-normalized) and a similarity matrix between the two KGs' not-yet-seeded entities, thresholded to keep only confident correlations -- exposed post-fit()asattr_sim_mat_/attr_pool1_ids_/attr_pool2_ids_(see below for why this is no longer used to perturb the structural embeddings).
Found while diagnosing #28's exhaustive-evaluation gap: OpenEA's own
reference jape.py builds a sim_optimizer (an apply_gradients op meant
to regularize ent_embeds toward the attribute-similarity blend each
epoch) but its launch_sim_1epo only ever fetches self.sim_loss via
session.run -- self.sim_optimizer is never passed to session.run
anywhere in the file. In TensorFlow 1.x an unfetched op simply never
executes, so OpenEA's own published JAPE numbers were produced by pure
structural-TransE training -- the attribute pipeline runs and computes a
loss value (used only for a print statement) that never actually touches
the entity embeddings. Confirmed empirically: running OpenEA's real
reference implementation reproduces its published Hits@1 (0.261 vs. target
0.263); a version of this port that does apply the attribute-similarity
gradient (either OpenEA's literal dense/thresholded matrix, or this port's
previous row-argmax-sparsified variant) scores far below both that number
and structural-only training under exhaustive ranking. This port now
matches OpenEA's real (not its apparent) behavior: fit() still trains
Attr2Vec and builds the attribute-similarity matrix -- exposed for
inspection -- but never uses it to update the structural embeddings.
Deviations from OpenEA's own code:
fit()gains two new optional parameters,attribute_triples1/attribute_triples2-- the paper's method genuinely doesn't fit the EA linker family's existing signature otherwise, since attribute triples are JAPE's whole point. Left asNone(default),fit()skips the attribute pipeline entirely (a no-op difference now that its output never affects training anyway). Most of this repo's EA dataset loaders have no attribute triples at all; see linkingtk.datasets.openea_native's module docstring for the one that does and why it's a separate loader family from linkingtk.datasets.openea's.- Attr2Vec's NCE loss uses uniform negative sampling, not OpenEA's
tf.nn.nce_lossdefault (a log-uniform sampler assuming frequency- sorted class ids, which our attribute-predicate ids aren't, and which PyTorch has no drop-in equivalent for) -- see_jape_torch.py'strain_attr2vecdocstring. - The attribute-similarity reference pool is "every
dataset1/dataset2entity not already aground_truthsource/target" rather than OpenEA's ownvalid_entities + test_entities-- the same deviation, for the same reason, already documented on IPTransELinker's bootstrap pool. Only affects the exposedattr_sim_mat_, not training.
JAPELinker ¶
JAPELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
neg_alpha: float = 0.1,
top_attr_threshold: float = 0.9,
attr_sim_mat_threshold: float = 0.95,
attr_max_epoch: int = 200,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space, plus an inspectable attribute similarity.
Must be fit before
link can be called. See
the module docstring for how this differs from
MTransELinker/
IPTransELinker, and
in particular why the attribute-similarity matrix built from
attribute_triples1/attribute_triples2 no longer perturbs the
trained structural embeddings (it's exposed post-fit() as
attr_sim_mat_ instead).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained structural and
attribute embeddings. OpenEA's published EN-FR-15K-V1 config
uses |
100
|
num_epochs
|
int
|
Structural training epochs. OpenEA's config allows up
to |
500
|
batch_size
|
int
|
Mini-batch size for both the structural triple loss and (if attribute triples are given) Attr2Vec's training. |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for every optimizer used
here -- OpenEA's published value is |
0.01
|
neg_alpha
|
float
|
Weight on the negative-triple term in the structural
loss ( |
0.1
|
top_attr_threshold
|
float
|
Fraction of each KG's distinct attribute
predicates to keep, by descending frequency. OpenEA's
published value is |
0.9
|
attr_sim_mat_threshold
|
float
|
Attribute-similarity values below this are
zeroed out in the exposed |
0.95
|
attr_max_epoch
|
int
|
Attr2Vec training epochs. OpenEA's published value
is |
200
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/jape.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
attr_pool1_ids_
instance-attribute
¶
attr_pool1_ids_: list[str] = []
Entity ids indexing attr_sim_mat_'s rows.
attr_pool2_ids_
instance-attribute
¶
attr_pool2_ids_: list[str] = []
Entity ids indexing attr_sim_mat_'s columns.
attr_sim_mat_
instance-attribute
¶
attr_sim_mat_: NDArray[floating[Any]] | None = None
Thresholded attribute-similarity matrix from the last fit() call, or None if no
attribute triples were given. Rows/columns correspond to
attr_pool1_ids_/attr_pool2_ids_. Diagnostic only -- see the
module docstring for why this doesn't affect the trained
embeddings.
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
attribute_triples1: list[Triple] | None = None,
attribute_triples2: list[Triple] | None = None,
) -> JAPELinker
Train a shared TransE space; optionally build an inspectable attribute-similarity matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
attribute_triples1
|
list[Triple] | None
|
KG1's |
None
|
attribute_triples2
|
list[Triple] | None
|
KG2's attribute triples. See
|
None
|
Returns:
| Type | Description |
|---|---|
JAPELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/jape.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/jape.py
468 469 470 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/jape.py
472 473 474 | |
KDCoE¶
kdcoe ¶
KDCoE knowledge-graph-embedding linker for Entity Alignment.
Chen, M., Tian, Y., Chang, K.-W., Skiena, S., & Zaniolo, C. (2018). Co-training Embeddings of Knowledge Graphs and Entity Descriptions for Cross-lingual Entity Alignment. IJCAI 2018. https://www.ijcai.org/proceedings/2018/0556.pdf
Like MTransELinker and
IPTransELinker, this is a
faithful reimplementation of KDCoE's actual training procedure, ported
directly from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/kdcoe.py
and its modules/base/losses.py, modules/bootstrapping/alignment_finder.py
support code) rather than from a from-scratch reading of the paper. KDCoE
co-trains two signals with a bootstrapping loop between them:
- A structural embedding: margin-based TransE (
margin=1.5, one negative per positive, corrupting within each triple's own KG -- reuses_iptranse_training.py'ssample_negative_triples) bridged across KGs by a learned mapping matrix, the same "mapping" alignment module MTransE uses (not IPTransE's/JAPE's shared-id "sharing" trick) -- entities keep separate per-KG embedding rows in one combined table. - A cross-lingual entity-description encoder: each entity's
description text (or, when it has none, its own label -- see
_kdcoe_text.extract_description_text) is embedded via pretrained word vectors, then run through a GRU -> Conv1D -> attention-pool -> GRU -> attention-pool -> dense pipeline (_kdcoe_torch.build_description_encoder), trained with an in-batch contrastive loss that pulls each known-aligned pair's description vectors together and pushes apart other pairs in the same batch.
Each outer co-training iteration trains the description encoder to
convergence, uses it to find new high-confidence pairs among entities
outside the seed set (row-wise best-match above desc_sim_th, reusing
_iptranse_training.py's find_new_pairs -- confirmed to be exactly
OpenEA's own find_potential_alignment_greedily/
find_alignment logic), trains the structural+mapping model to
convergence (now also pulled toward those newly found pairs via a
second, new_param-weighted mapping loss), and uses it to find new
pairs the same way -- stopping when a round re-discovers nothing beyond
what's already known (max_co_training_iters caps the worst case).
Key finding shared with
MTransELinker: both
mapping-loss optimizers (the seed-pair one and the newly-bootstrapped-pair
one) must update the shared entity embeddings as well as the mapping
matrix, matching OpenEA's generate_optimizer(loss, lr, var_list=None,
...) -- leaving var_list unset makes TensorFlow differentiate
against every trainable variable the loss touches, not just the mapping
matrix. Restricting either optimizer here to just the mapping matrix
converges roughly an order of magnitude slower.
Key finding from reading the reference source, not obvious from the
paper: _get_desc_input doesn't require every entity to have a real
description triple -- entities without one fall back to their own local
name as pseudo-description text. Since this repo's dataset loaders already
populate Entity.labels with exactly that fallback text (see
linkingtk.datasets.openea_native), the description pathway here always
has some text to work with. Unlike
JAPELinker, where omitting
attribute_triples1/attribute_triples2 disables the attribute
pathway entirely, here it only reduces the description signal to
label-only text -- worth noting since it's a real interface difference
within this linker family.
Word embeddings: pretrained vectors are required (the description
encoder's whole point is leveraging generic-language priors that aren't
learnable from a 15K-entity alignment task alone). This fetches the same
file OpenEA's own published config uses --
wiki-news-300d-1M.vec.zip from fastText's stable, Facebook-hosted URL
-- via linkingtk.datasets._util's fetch_cached, the same mechanism
every dataset loader in this repo already uses. It's a much larger
one-time download (~681MB) than any other asset this repo fetches, so
_kdcoe_text.py's load_fasttext_vectors streams the archived
.vec member line-by-line and keeps only rows whose word is in this
run's own description vocabulary, rather than materializing the full
~1M-word file.
Deviations beyond what _kdcoe_text.py/_kdcoe_torch.py already
document:
- Reference pool for bootstrapping: OpenEA restricts the "not yet
aligned" search pool to its own dataset loader's
valid_entities + test_entitiesspecifically. This repo'sfit()only receivesdataset1/dataset2andground_truth(the train split), so the pool here is every entity not already aground_truthsource/target -- broader than OpenEA's pool, same already-established deviation as IPTransELinker's bootstrap pool and JAPELinker's attribute reference pool (reuses_jape_training.py'sreference_poolsdirectly). - Final scoring uses the structural+mapping embedding only: the
description encoder drives bootstrapping during
fit()but doesn't contribute tolink()'s own similarity score --link()/_embedding()/_project()are near-identical to MTransE's. - Mapping-loss training samples with replacement each step rather
than OpenEA's
random.sample(already-established MTransE deviation, same rationale). - Early stopping uses this repo's own plain patience-counter Hits@1 check
(twice -- once per phase, each reset every outer co-training iteration,
matching OpenEA's own per-phase
flag1/flag2reset) rather than OpenEA's exact two-flagearly_stop(). - Description-found bootstrap pairs use reciprocal best-match, not
desc_sim_th. Diagnosed while benchmarking on real EN-FR-15K-V1 data (#29): the description encoder's cosine similarities saturate near1.0for nearly every reference-pool pair regardless of correctness (median ~0.9998 in testing), so an absolute threshold likedesc_sim_th=0.95accepts ~99.9% of the pool each round and can't discriminate anything --find_new_pairs's row-argmax+threshold (OpenEA's ownfind_potential_alignment_greedily) isn't a usable filter once similarities are this uniform. This dataset's real.../descriptiontriple coverage is also sparse (10.5% of KG1 entities, 0.5% of KG2), so most descriptions are single-word label fallbacks -- but 60.7% of EN-FR-15K-V1's ground-truth pairs share a literally identical local entity name across languages (OpenEA's own documented v1-dataset "name bias"; their README recommends v2.0 to remove it), which the label-fallback pathway can exploit even without rich text: the description encoder's own validation Hits@1 is a real 44-46% across co-training iterations, well above chance. The earlier fix here (before this reciprocal-match change) fully separated description-found pairs from structural training to stop the ~99.9% false-positive rate from collapsing structural Hits@1 from a clean ~0.40 to near-zero -- but that also meant the description encoder's genuine 44-46% signal could never improve the final structural+mapping embedding at all. Switching the description pathway's bootstrap rounds to_iptranse_training.find_mutual_pairs(reciprocal row/column argmax agreement, which stays discriminative under saturation since the argmax positions still vary even when the values don't) raises precision enough that description-found pairs are now merged back into the same pool that feeds the structural mapping-new loss, matching OpenEA's own unifiedself.new_alignment/self.new_alignment_indexdesign rather than permanently working around it.
KDCoELinker ¶
KDCoELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
alpha: float = 5.0,
new_param: float = 0.1,
wv_dim: int = 300,
default_desc_length: int = 4,
desc_batch_size: int = 512,
desc_sim_th: float = 0.95,
sim_th: float = 0.8,
max_co_training_iters: int = 5,
word_embed_url: str = _FASTTEXT_URL,
cache_dir: Path | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Co-trains a structural TransE+mapping space with a description-text encoder.
Must be fit before link can be called. See the module docstring for how this differs from MTransELinker and IPTransELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings and the (square) mapping matrix. OpenEA's published
EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Per-phase training-epoch cap (structural or
description), each co-training iteration. OpenEA's config
allows up to |
500
|
batch_size
|
int
|
Mini-batch size for structural triple-loss training; the mapping-loss batch size is derived from it (same number of minibatches per epoch for both, matching OpenEA). |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for every optimizer here
(structural, mapping, mapping-new, description) -- OpenEA's
published value is |
0.01
|
margin
|
float
|
Hinge margin for the structural triple loss. OpenEA's
published value is |
1.5
|
alpha
|
float
|
Weight applied to the seed-pairs mapping loss. OpenEA's
published value is |
5.0
|
new_param
|
float
|
Weight applied to the bootstrapped-pairs mapping loss.
OpenEA's published value is |
0.1
|
wv_dim
|
int
|
Pretrained word-vector dimensionality. Must match
|
300
|
default_desc_length
|
int
|
Fixed word-sequence length each entity's
description is padded/truncated to. OpenEA's published value
is |
4
|
desc_batch_size
|
int
|
Mini-batch size for description-encoder training.
OpenEA's published value is |
512
|
desc_sim_th
|
float
|
Kept for interface parity with OpenEA's published
value ( |
0.95
|
sim_th
|
float
|
Minimum (mapped) structural-embedding similarity for a
bootstrap round to accept a newly found pair. OpenEA's
published value is |
0.8
|
max_co_training_iters
|
int
|
Cap on outer co-training iterations (each
alternating a description-encoder phase and a structural
phase). OpenEA's published value ( |
5
|
word_embed_url
|
str
|
URL of a fastText-format |
_FASTTEXT_URL
|
cache_dir
|
Path | None
|
Passed through to |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/kdcoe.py
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
attribute_triples1: list[Triple] | None = None,
attribute_triples2: list[Triple] | None = None,
) -> KDCoELinker
Co-train the structural+mapping space and the description encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping in both phases -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
attribute_triples1
|
list[Triple] | None
|
KG1's |
None
|
attribute_triples2
|
list[Triple] | None
|
KG2's attribute triples. See
|
None
|
Returns:
| Type | Description |
|---|---|
KDCoELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/kdcoe.py
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side (projected).
Source code in src/linkingtk/algorithms/ea/kdcoe.py
715 716 717 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side (unprojected).
Source code in src/linkingtk/algorithms/ea/kdcoe.py
719 720 721 | |
_kdcoe_text ¶
Private, plain-numpy (torch-free) helpers for KDCoELinker.
Ported from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA -- approaches/kdcoe.py's
_get_desc_input/_get_local_name_by_name_triple). Independently
testable without torch installed -- see _kdcoe_torch.py for the
description-encoder network and training-step functions.
build_word_ids ¶
build_word_ids(
tokens_by_entity: dict[str, list[str]],
word_to_index: dict[str, int],
default_desc_length: int,
oov_id: int,
) -> dict[str, npt.NDArray[np.int64]]
Each entity's tokens -> a fixed-length array of word ids.
Ports lookup_and_padding: truncates to default_desc_length
words, padding with oov_id if shorter. A token absent from
word_to_index also maps to oov_id -- the same handling OpenEA
gives words unseen in the pretrained vectors (see
load_fasttext_vectors).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tokens_by_entity
|
dict[str, list[str]]
|
Per-entity token lists, e.g. from tokenizing extract_description_text's output. |
required |
word_to_index
|
dict[str, int]
|
Vocabulary word -> row index (e.g. into a pretrained embedding table). |
required |
default_desc_length
|
int
|
Fixed output length. OpenEA's published
value is |
required |
oov_id
|
int
|
Id used for padding and out-of-vocabulary words -- conventionally one past the real vocabulary's last index. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, NDArray[int64]]
|
|
Source code in src/linkingtk/algorithms/ea/_kdcoe_text.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | |
extract_description_text ¶
extract_description_text(
entities: list[Entity], attribute_triples: list[Triple]
) -> dict[str, str]
Each entity's description text, falling back to its own label.
Ports _get_desc_input's entity-description selection: an entity's
text is the first attribute-triple value whose predicate URI contains
"escription" (catching description/Description, e.g.
dc:description, dbo:description) -- the same substring check
OpenEA's own code uses. Entities with no such triple fall back to
their own local name (_get_local_name_by_name_triple's fallback --
this repo's dataset loaders already populate Entity.labels with
exactly that text, see linkingtk.datasets.openea_native, so no
separate name-triple lookup is needed here).
This is why the description pathway never strictly needs
attribute_triples to produce some text for every entity -- see
KDCoELinker's docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entities
|
list[Entity]
|
Every entity needing description text (both KGs, concatenated by the caller). |
required |
attribute_triples
|
list[Triple]
|
|
required |
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
|
dict[str, str]
|
Source code in src/linkingtk/algorithms/ea/_kdcoe_text.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
load_fasttext_vectors ¶
load_fasttext_vectors(
zip_url: str,
vocabulary: set[str],
cache_dir: Path | None = None,
) -> dict[str, npt.NDArray[np.floating[Any]]]
Pretrained word vectors for vocabulary's words only, from a fastText .vec.zip.
Fetches zip_url via fetch_cached (linkingtk.datasets._util
-- cached on disk after the first call; file:// urls, used by
tests to point at a tiny local fixture, are never cached, matching
fetch_cached's existing behavior) and streams the archived
.vec member line-by-line (zipfile.ZipFile(...).open(member))
rather than decoding the whole decompressed text at once -- for the
real fastText file (wiki-news-300d-1M.vec, ~1M words, ~2.3GB
decompressed) this keeps peak new memory bounded by len(vocabulary)
rather than the file's full vocabulary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
zip_url
|
str
|
URL of a zip containing exactly one |
required |
vocabulary
|
set[str]
|
Words to keep; every other row is skipped without being parsed into an array. |
required |
cache_dir
|
Path | None
|
Passed through to |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, NDArray[floating[Any]]]
|
|
dict[str, NDArray[floating[Any]]]
|
both in |
dict[str, NDArray[floating[Any]]]
|
absent from the file are simply omitted -- callers treat a |
dict[str, NDArray[floating[Any]]]
|
missing word as out-of-vocabulary, same as |
dict[str, NDArray[floating[Any]]]
|
|
dict[str, NDArray[floating[Any]]]
|
|
Source code in src/linkingtk/algorithms/ea/_kdcoe_text.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | |
tokenize_description ¶
tokenize_description(text: str) -> list[str]
Lowercased, punctuation-stripped whitespace tokens.
Ports the string.punctuation regex replace + whitespace split
OpenEA applies to both description and local-name text before word-id
lookup.
Source code in src/linkingtk/algorithms/ea/_kdcoe_text.py
79 80 81 82 83 84 85 86 87 | |
AttrE¶
attre ¶
AttrE knowledge-graph-embedding linker for Entity Alignment.
Trisedya, B. D., Qi, J., & Zhang, R. (2019). Entity Alignment between Knowledge Graphs Using Attribute Embeddings. AAAI 2019. https://people.eng.unimelb.edu.au/jianzhongq/papers/AAAI2019_EntityAlignment.pdf
Like IPTransELinker and
JAPELinker, this is a faithful
reimplementation of AttrE's actual training procedure, ported directly
from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/attre.py
and its modules/train/batch.py support code) rather than from a
from-scratch reading of the paper.
Important finding from reading the reference source, not obvious from a one-line method summary: despite AttrE's paper framing it as aligning entities "using attribute triples only," OpenEA's actual benchmarked implementation trains a full structural TransE component over relation triples too, alongside a separate character/attribute-embedding (CE) space -- the two are cross-regularized by a joint cosine-similarity loss. This is not an optional add-on; it's what OpenEA's own published number comes from, so this port includes both halves rather than a structure-free implementation. Two embedding spaces, trained jointly:
- Structural (SE):
ent_embeds/rel_embeds, plain margin-based TransE + negative sampling over relation triples -- reuses_kdcoe_torch.py'sKGContext,build_kg_context, andtrain_structural_epochdirectly (already fully generic, no path loss, no mapping). - Character/attribute (CE):
ent_embeds_ce(a second, independent entity embedding table, sharing the same ids asent_embeds),attr_embeds(one row per attribute predicate),char_embeds(one row per selected character). Attribute triples are treated as TransE-style triples --entity_ce + attribute ≈ compose(value's characters)-- with the value's embedding built compositionally from its characters (see_attre_torch.py'scompose_value_embeddings). Negative sampling corrupts only the entity endpoint (see_attre_text.py'ssample_negative_attribute_triples).
Alignment module is "sharing" (attre_args_15K.json), identical to
IPTransE's/JAPE's mechanism -- seed pairs get one shared embedding row (in
both the SE and CE tables), no mapping matrix. This is the only
cross-KG alignment signal; there is no separate seed-pair loss anywhere
in AttrE's own code. Reuses _iptranse_training.py's
build_shared_id_mappings directly.
Joint loss: sum(1 - cos_sim(ent_embeds[e], ent_embeds_ce[e])) over
every entity in both KGs (not just seed pairs) -- this is what actually
ties the two spaces together, pulling both toward mutual agreement rather
than fixing one side to a projection of the other (contrast
MTransELinker's mapping
loss, which only updates its mapping matrix). See _attre_torch.py's
train_joint_epoch for a note on a curiosity ported literally from
OpenEA's own code (it re-runs this loss over the entire entity list
multiple times per epoch, not sub-batched).
No bootstrapping, no co-training loop -- simpler control flow than KDCoELinker. Per epoch: one SE epoch, one CE epoch, one joint-loss pass, then periodic validation.
optimizer="SGD" in OpenEA's published config -- the first method in
this family to use plain SGD rather than Adagrad; ported literally
(torch.optim.SGD).
Deviations beyond what _attre_text.py/_attre_torch.py already
document:
attribute_triples1/attribute_triples2are required, not optional -- unlike JAPELinker (structural-only fallback) and KDCoELinker (label-fallback always gives some description text), AttrE has no meaningful degraded mode without attribute triples -- the entire CE half and joint loss depend on them, and there's no fallback text source.fit()raisesLinkingTKErrorif both are empty.- Final scoring uses the structural (SE) embedding only: same
precedent as every other linker in this family that has more than one
trained representation --
ent_embeds_ceregularizesent_embedsduring training via the joint loss but isn't itself used bylink(). - Early stopping uses this repo's own plain patience-counter Hits@1 check
rather than OpenEA's own two-flag
early_stop(), and doesn't gate on OpenEA'sstart_valid(checks are simply skipped untilval_ground_truthis given andeval_everydivides the epoch, same convention as every other linker in this family).
AttrELinker ¶
AttrELinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
literal_len: int = 5,
char_freq_threshold: float = 0.0001,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space regularized by character embeddings.
Must be fit before link can be called. See the module docstring for how this differs from IPTransELinker and JAPELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of every trained embedding table
(entities, relations, character-space entities, attributes,
characters). OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Training epochs, each one pass of structural training,
one pass of attribute/character training, and one (repeated)
pass of joint-loss training. OpenEA's config allows up to
|
500
|
batch_size
|
int
|
Mini-batch size for both the structural and attribute-triple training (each derives its own step count from its own triple count), and the divisor for the joint loss's repeat count. |
5000
|
learning_rate
|
float
|
SGD's learning rate, for every optimizer here.
OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for both the structural and attribute-triple
losses. OpenEA's published value is |
1.5
|
literal_len
|
int
|
Fixed character-sequence length each attribute value
is truncated/padded to. OpenEA's published value is |
5
|
char_freq_threshold
|
float
|
Minimum share of total (distinct-value)
character occurrences required to keep a character in the
vocabulary; rarer characters map to the padding id. OpenEA's
published value is |
0.0001
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/attre.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
attribute_triples1: list[Triple],
attribute_triples2: list[Triple],
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> AttrELinker
Train a shared TransE space jointly with a character/attribute embedding space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
attribute_triples1
|
list[Triple]
|
KG1's |
required |
attribute_triples2
|
list[Triple]
|
KG2's attribute triples. Required. |
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
AttrELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/attre.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/attre.py
454 455 456 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/attre.py
458 459 460 | |
IMUSE¶
imuse ¶
IMUSE knowledge-graph-embedding linker for Entity Alignment.
He, F., Li, Z., Qiao, Q., Liu, A., & Zhao, L. (2019). Unsupervised Entity Alignment Using Attribute Triples and Relation Triples. DASFAA 2019. https://link.springer.com/content/pdf/10.1007%2F978-3-030-18576-3_22.pdf
Like KDCoELinker and
AttrELinker, this is a
faithful reimplementation of IMUSE's actual training procedure, ported
directly from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/imuse.py,
its modules/base/initializers.py/modules/base/losses.py, and
models/basic_model.py) rather than from a from-scratch reading of the
paper.
IMUSE is unsupervised -- unlike every other linker in this family,
fit takes no
ground_truth parameter at all. Reading imuse.py confirms there's
no seed-pair argument anywhere in OpenEA's own IMUSE class:
- Bootstrap alignment from attribute string similarity, before any
embedding training starts (ports
interactive_model): entity attribute predicates are paired across KGs by Levenshtein-ratio similarity of their URIs' local names, kept to the 10 pairs with the most combined triple support; entities are then paired by the average Levenshtein ratio of their values under those aligned predicates. This fixed set is IMUSE's only source of cross-KG signal. - Likely bug found while reading the reference source, not obvious from
a paper summary: OpenEA's own
run_one_ea/run_one_aeaccept a candidate match inside the inner per-candidate scan loop, on every improving match, rather than after scanning all candidates -- so an earlier, since-superseded match for the same entity stays in the result set and permanently claims its target, even after a strictly better match is found later in the same scan. This is inconsistent with the textbook-correct version of the identical greedy-accept pattern used a few dozen lines away in the same file (get_aligned_attr_pair_by_name_similarity). Treated as an unintentional implementation bug rather than a deliberate design choice -- unlike this family's usual "port literal quirks faithfully" posture for behavior that's actually load-bearing for a published number (e.g. this linker's own full-list epoch re-runs, below) -- this port accepts only the single best match found after a full scan. See_imuse_text.py'salign_entities_by_attribute_valuesdocstring. - Tractability: OpenEA brute-forces every candidate entity pair
(~225M comparisons at this dataset's scale), parallelized across 8
processes. A single-process port instead indexes attribute values per
KG and only scores entity pairs that share at least one value (after
casefolding) under an aligned predicate -- entities with no shared
aligned-predicate value can only ever score
0similarity anyway, so this loses no correct matches from OpenEA's own similarity formula, only ones whose only signal is a near-but-not-exact value match. See_imuse_text.py's_value_indexdocstring. - A real, diagnosed benchmark finding: at OpenEA's own hardcoded
0.6name-similarity threshold, live EN-FR-15K-V1 data lets a spurious attribute-predicate pair through --.../ontology/games/foaf/0.1/namescore0.667on local-name Levenshtein ratio (genuinely above0.6, not a bug), and having more combined triple support than the correct self-matchfoaf/0.1/name/foaf/0.1/name, it wins that predicate's top-10 slot instead. Verified directly (not assumed): this one wrong predicate pair drags the entity-bootstrap step's precision against this dataset's own known links down to 54.7%; raisingname_sim_thresholdto0.9removes it and raises precision to 82.1%, taking live Hits@1 from0.470to0.571with no other change. This is very likely a rehost-specific attribute-predicate-vocabulary difference from OpenEA's own original dataset files (not present in whatever they benchmarked on), not a bug in this port -- seeexamples/imuse_benchmark.py, which usesname_sim_threshold=0.9for exactly this reason, while the class default here stays at OpenEA's own literal0.6. - Structural (SE) half: once bootstrapping is done, IMUSE trains a
margin-based TransE embedding over relation triples -- the same shape
every linker in this family uses, reusing
_kdcoe_torch.py'sKGContext/build_kg_context/train_structural_epochdirectly. Entity/relation ids come from one combined id space across both KGs (like MTransELinker/ KDCoELinker), not the shared-id "sharing" merge IPTransELinker/ JAPELinker/ AttrELinker use -- IMUSE never merges rows for aligned pairs; alignment is a separate loss term (next point). - Alignment loss -- the genuinely new part: unlike every other linker
here, IMUSE has no mapping matrix and no shared-id merge. Instead,
a direct
sum(||ent_embeds[e1] - ent_embeds[e2]||^2)loss pulls the two (still separate-row) embeddings of each bootstrapped pair together. OpenEA's ownlaunch_align_training_1epore-runs this over the full bootstrapped-pair liststepstimes per epoch rather than sub-batching it -- the same full-list-re-run quirk AttrELinker'strain_joint_epochalready documents; ported the same way here (see_imuse_torch.py'strain_align_epoch). optimizer="SGD"-- like AttrE, not Adagrad (this family's more common default).- Even though training is unsupervised, early stopping is not:
val_ground_truthis still accepted and used exactly like every other linker here (OpenEA's ownrun()checks Hits@1 againstkgs.valid_linkseveryeval_freqepochs) -- the "unsupervised" claim is specifically about training signal, not about early stopping.
New dependency: rapidfuzz (added to the kge optional-dependency
group alongside pykeen/torch) for Levenshtein-ratio string
similarity -- OpenEA's own bootstrap relies on python-Levenshtein's
.ratio(); rapidfuzz.distance.Indel.normalized_similarity computes
the identical formula (rapidfuzz.distance.Levenshtein alone uses a
different substitution cost/normalization -- see _imuse_text.py's
levenshtein_ratio docstring). Imported lazily (inside functions, not
at module top) in _imuse_text.py, matching this package's established
torch-avoidance convention, so importing linkingtk.algorithms.ea
doesn't require the kge extra just to reach an unrelated linker.
Same dataset note as JAPE/KDCoE/AttrE: this uses
EnFr15KAttrDataset,
not EnFr15KDataset, since IMUSE's entire mechanism depends on attribute
triples and that loader has none.
IMUSELinker ¶
IMUSELinker(
embedding_dim: int = 100,
num_epochs: int = 2000,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
name_sim_threshold: float = 0.6,
entity_sim_threshold: float = 0.6,
attr_sim_threshold: float = 0.6,
top_k_attribute_pairs: int = 10,
bootstrap_iterations: int = 1,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Unsupervised EA: bootstraps its own seed pairs from attribute-value similarity.
Must be fit before
link can be called. Unlike
every other linker in this family, fit
takes no ground_truth -- see the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Training epochs, each pairing one epoch of structural
triple-loss training with one epoch of alignment-loss
training. OpenEA's config allows up to |
2000
|
batch_size
|
int
|
Mini-batch size for structural triple-loss training; the alignment-loss step count is derived from it (matching OpenEA). |
5000
|
learning_rate
|
float
|
SGD's learning rate, for both optimizers --
OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for the structural triple loss. OpenEA's
published value is |
1.5
|
name_sim_threshold
|
float
|
Minimum Levenshtein ratio for two attribute
predicates' local names to be considered aligned. OpenEA
hardcodes |
0.6
|
entity_sim_threshold
|
float
|
Minimum average Levenshtein ratio (across
shared aligned-predicate values) for two entities to be
considered aligned. OpenEA's published value
( |
0.6
|
attr_sim_threshold
|
float
|
Same as
|
0.6
|
top_k_attribute_pairs
|
int
|
Maximum aligned attribute-predicate pairs
kept after name-similarity matching, ranked by combined triple
count. OpenEA hardcodes |
10
|
bootstrap_iterations
|
int
|
Outer bootstrap rounds (entity alignment,
then optionally attribute-predicate re-alignment from those
entities). OpenEA's published default
( |
1
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/imuse.py
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
graph: Graph,
attribute_triples1: list[Triple],
attribute_triples2: list[Triple],
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> IMUSELinker
Bootstrap an alignment from attribute similarity, then train jointly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
attribute_triples1
|
list[Triple]
|
KG1's |
required |
attribute_triples2
|
list[Triple]
|
KG2's attribute triples. See
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
IMUSELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If both |
OptionalDependencyError
|
If torch or rapidfuzz isn't installed. |
Source code in src/linkingtk/algorithms/ea/imuse.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/imuse.py
434 435 436 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/imuse.py
438 439 440 | |
MultiKE¶
multike ¶
MultiKE knowledge-graph-embedding linker for Entity Alignment.
Zhang, Q., Sun, Z., Hu, W., Chen, M., Guo, L., & Qu, Y. (2019). Multi-view Knowledge Graph Embedding for Entity Alignment. IJCAI 2019. https://www.ijcai.org/proceedings/2019/0754.pdf
By far the most complex method in this family (see
MTransELinker,
IPTransELinker,
JAPELinker,
KDCoELinker,
AttrELinker,
IMUSELinker) -- a faithful
reimplementation of MultiKE's actual training procedure, ported directly
from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/multi_ke.py,
931 lines, read in full, plus literal_encoder.py,
predicate_alignmnet.py, modules/base/losses.py,
modules/load/read.py, models/basic_model.py) rather than a
from-scratch paper reading.
MultiKE is supervised (unlike IMUSE): seed pairs are its only real
supervision channel, via literal entity-id substitution into each KG's own
triples (see _multike_text.generate_cross_kg_relation_triples/
generate_cross_kg_attribute_triples, ported from read.py's
generate_sup_relation_triples/generate_sup_attribute_triples).
Architecture: three views unified into one shared table¶
- Name view (
name_embeds): a literal-text embedding of each entity's local name, computed once and frozen for the rest of training. See "The literal encoder" below for the one place this port genuinely departs from a faithful reimplementation. - Relation view (
rv_ent_embeds/rel_embeds): logistic-loss TransE over relation triples (softplus(||h+r-t||^2) + softplus(-||h'+r-t'||^2)) -- not the margin-based loss every other method in this family uses; MultiKE hardcodes this, ignoring the reference's own configurableargs.loss. - Attribute view (
av_ent_embeds/attr_embeds): a small CNN (_multike_torch.build_attribute_scorer) predicts a "head" representation from(attribute, value)pairs, scored via negative squared distance against the real head. - Shared space (
ent_embeds, used for every eval call -- confirmed inbasic_model.py'svalid()/test(), both always readself.ent_embedsregardless of which views exist): trained by alignment terms baked directly into the relation/attribute view losses and a separate common-space-learning step (_multike_torch.train_common_space_epoch) pullingent_embedstoward all three views for every entity, at its own learning rate (common_space_learning_rate, distinct from the mainlearning_rate). - Cross-KG entity inference (the actual supervision channel): for
each seed pair, every KG1 triple touching the source entity is
relabeled with the target's id and trained back into the shared
tables -- see
_multike_text.py's substitution functions. - Predicate soft-alignment: relation and attribute predicates are
aligned across KGs by mutual-best-match Levenshtein ratio on local
names (
_multike_text.align_predicates_by_name), feeding a per-triple weight in the attribute view's main loss and two more cross-KG inference loss terms, gated to start atstart_predicate_soft_alignment.
Three deliberate scope cuts, documented plainly¶
- Space mapping (
nv_mapping/rv_mapping/av_mappingorthogonal matrices,train_shared_space_mapping_1epo): defined in the reference'sinit()but the training loop that calls it is commented out in its ownrun()(multi_ke.py:923-926) -- genuinely dead code in the reference's own published run, confirmed by readingrun()itself. Not ported at all. - Periodic predicate-alignment re-estimation
(
update_predicate_alignment/find_predicate_alignment_by_embedding, every 10 epochs): refines the Levenshtein-based predicate pairs using the training embeddings' own cosine similarity. This port computes the Levenshtein-based alignment once atfit()start and keeps it fixed for the whole run (mirrors IMUSE's own fixed-bootstrap precedent) -- a refinement pass over an already-working initial signal, not the primary mechanism. - Truncated/nearest-neighbor negative sampling: not ported, matching every other linker in this family's established uniform-negative-sampling precedent (not a new deviation specific to MultiKE).
Also confirmed by reading every loss graph in full (not obvious from a
method-name skim, see _multike_torch.py's module docstring for the
full breakdown): only the main relation-view loss ever uses negative
sampling -- every other loss (attribute view, all four cross-KG
inference terms) is purely positive/regression-style. And
PredicateAlignModel.relation_triples_w_weights is computed by the
reference but never actually consumed anywhere in its own training
methods -- not ported, since it's unused in the reference itself.
The literal encoder¶
OpenEA's own name_embeds/literal_embeds come from a pretrained
English-only word2vec/fastText file plus a custom stacked autoencoder
trained via reconstruction loss to compress bag-of-word-vectors down to
embedding_dim. This port instead uses a small pretrained
multilingual transformer (transformers -- already a hard
dependency of this repo, zero new install) via
_multike_literal.encode_literals: mean-pooled over the attention
mask, projected down to embedding_dim via a fixed random
orthogonal projection (no training loop needed -- the transformer
already produces well-formed semantic structure). Real multilingual
semantic signal for cross-lingual name matching (e.g.
"United States"/"États-Unis"), which an English-only fastText file
(_kdcoe_text.py's existing load_fasttext_vectors infrastructure
was considered and is a real precedent in this repo, but doesn't fit a
cross-lingual signal) or a from-scratch in-corpus word2vec couldn't
provide. Default model: distilbert-base-multilingual-cased. First run
downloads it from the Hub (~540MB), cached afterward.
Other implementation notes¶
- Combined, not shared-id, entity space: like MTransELinker/ KDCoELinker/ IMUSELinker -- MultiKE never merges seed pairs into one row; alignment is entirely loss-driven.
- Which embedding tables get L2-normalized on every read isn't
uniform --
rv_ent_embeds/rel_embeds/av_ent_embeds/ent_embedsalways are;attr_embedsnever is (ported from the reference's ownxavier_init(..., is_l2_norm=False)for that one table); see_multike_torch.py's module docstring for the full detail. optimizer="SGD", matching AttrE/IMUSE's precedent in this family.- Final scoring uses the shared
ent_embedsdirectly, cosine similarity, no mapping -- matchesbasic_model.py'svalid()/test()always readingself.ent_embeds. attribute_triples1/attribute_triples2are required (raisesLinkingTKErrorif both empty) -- the attribute view, the literal encoder's value vocabulary, and the predicate soft-alignment mechanism all depend on them, same posture as AttrE.- Early stopping uses this repo's own plain patience-counter Hits@1 check
rather than OpenEA's own two-flag
early_stop(), and doesn't gate on OpenEA'sstart_valid, matching every other linker in this family.
MultiKELinker ¶
MultiKELinker(
embedding_dim: int = 100,
num_epochs: int = 2000,
batch_size: int = 5000,
entity_batch_size: int = 5000,
attribute_batch_size: int = 5000,
learning_rate: float = 0.001,
common_space_learning_rate: float = 0.004,
neg_triple_num: int = 10,
cv_weight: float = 1.0,
predicate_init_sim: float = 0.9,
predicate_soft_sim: float = 0.8,
start_predicate_soft_alignment: int = 10,
min_predicate_triple_count: int = 10,
literal_encoder_model: str = "distilbert-base-multilingual-cased",
literal_max_length: int = 16,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared embedding table unifying name/relation/attribute views.
Must be fit before link can be called. See the module docstring for the full architecture and the three documented scope cuts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of every trained embedding table.
OpenEA's published EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Training epochs, each one pass of relation-view,
common-space, cross-KG relation-entity (and, once warmed up,
cross-KG relation-predicate) training, then the same four
steps for the attribute view. OpenEA's config allows up to
|
2000
|
batch_size
|
int
|
Mini-batch size for the relation view's main loss and its two cross-KG inference terms. |
5000
|
entity_batch_size
|
int
|
Mini-batch size for common-space learning. |
5000
|
attribute_batch_size
|
int
|
Mini-batch size for the attribute view's main loss and its two cross-KG inference terms. |
5000
|
learning_rate
|
float
|
SGD's learning rate for every optimizer except
common-space learning. OpenEA's published value is |
0.001
|
common_space_learning_rate
|
float
|
SGD's learning rate for common-space
learning specifically (OpenEA's own |
0.004
|
neg_triple_num
|
int
|
Negatives sampled per positive relation triple --
only the main relation-view loss uses negative sampling at
all (see the module docstring). Published value is |
10
|
cv_weight
|
float
|
Weight applied to the common-space-learning loss.
Published value is |
1.0
|
predicate_init_sim
|
float
|
Levenshtein-ratio threshold (of humanized
local names) for the one-time mutual-best-match predicate
alignment. Published value is |
0.9
|
predicate_soft_sim
|
float
|
Similarity floor used to rescale an aligned
predicate pair's weight ( |
0.8
|
start_predicate_soft_alignment
|
int
|
Epoch (0-indexed) at which the two
predicate-alignment cross-KG loss terms start contributing --
a warm-up gate. Published value is |
10
|
min_predicate_triple_count
|
int
|
Attribute predicates with fewer than
this many combined (both-KG) triples are dropped before
anything else. Published value is |
10
|
literal_encoder_model
|
str
|
Hugging Face model id for the literal
encoder (see the module docstring's "The literal encoder"
section). Defaults to |
'distilbert-base-multilingual-cased'
|
literal_max_length
|
int
|
Token-truncation length passed to the literal encoder for every name/value string. |
16
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/multike.py
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
attribute_triples1: list[Triple],
attribute_triples2: list[Triple],
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> MultiKELinker
Train the three-view embedding and a shared space unifying them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
attribute_triples1
|
list[Triple]
|
KG1's |
required |
attribute_triples2
|
list[Triple]
|
KG2's attribute triples. Required. |
required |
random_state
|
int | None
|
Seed for reproducible training (also seeds the literal encoder's random projection). Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
MultiKELinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/multike.py
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/multike.py
752 753 754 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/multike.py
756 757 758 | |
RSN4EA¶
rsn4ea ¶
RSN4EA knowledge-graph-embedding linker for Entity Alignment.
Guo, L., Sun, Z., & Hu, W. (2019). Learning to Exploit Long-term Relational Dependencies in Knowledge Graphs. ICML 2019. http://proceedings.mlr.press/v97/guo19c/guo19c.pdf
Structurally the most different method in this family: instead of a
triple-local translational embedding (TransE-style, like every other
linker in linkingtk.algorithms.ea), RSN4EA trains a Recurrent
Skipping Network -- an LSTM run over sampled random-walk paths through
the combined graph, with a next-token prediction loss (entity/relation
language-modeling, not a distance loss), and a residual "skip" connection
letting each relation-step's prediction shortcut directly from the
preceding entity's embedding. Cross-KG alignment comes from a
data-construction trick, not a mapping matrix or a shared-id merge: seed
pairs make their two entity ids mutually substitutable throughout the
walkable graph, so a random walk that reaches one can carry on through the
other KG's own edges from that point.
Like MTransELinker, this
is a faithful reimplementation of RSN4EA's actual training procedure,
ported directly from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/rsn4ea.py)
rather than from a from-scratch reading of the paper. Several of OpenEA's
published config values turn out to be dead -- never actually consulted by
rsn4ea.py's own code -- and this port follows what's actually executed:
ent_l2_norm/rel_l2_normaretruein OpenEA's config, but_define_variablesbuilds embeddings via plaintf.get_variable(..., xavier_initializer), never through theinit_embeddings()utility that appliesl2_normalize-- unlike every other linker in this family, RSN4EA's embeddings are never L2-normalized, at init or per-forward-pass."optimizer": "Adagrad"in the config, but_define_variableshardcodestf.train.AdamOptimizer(options.learning_rate)directly, bypassinggenerate_optimizer(..., opt=self.args.optimizer)-- Adam is what actually runs, so this port uses Adam unconditionally (not a constructor option)."dim": 100is never read anywhere inrsn4ea.py-- embedding size is driven entirely byhidden_size.add_weight'sw_h/w_r/w_talias-tracking columns are computed but never consumed bysample_paths/cal_loss-- only the structural effect of alias substitution (which triples exist) is load-bearing.add_weight's relation-alias variants are always empty (RSN4EA's ownrel_mappingis always an empty table -- no relation-alignment signal exists for this task), so only the 4 live head/tail-alias variants are ported (see_rsn4ea_training.build_augmented_kb).alignment_module: "mapping"has zero effect on RSN4EA's own training (run()fully overridesBasicModel.run(), never callinglaunch_training_1epo's mapping-loss branch) -- it only matters at the id-space level, which is exactlybuild_id_mappings's disjoint-per-KG shape (matching MTransELinker/ SEALinker), used here too.
Ported into two private helper modules: _rsn4ea_training.py (plain
numpy -- graph augmentation and path sampling, independently testable
without torch) and _rsn4ea_torch.py (the model and training-step
functions that build/consume PyTorch tensors).
Deliberate deviations from OpenEA's own code, beyond what those two modules already document:
- Full softmax cross-entropy instead of
tf.nn.nce_loss. OpenEA's NCE loss approximates a full softmax via log-uniform negative sampling -- a 2019-TF1-era efficiency hack for large vocabularies. At this dataset's real scale (~15K entities, low hundreds of relations doubled), OpenEA's ownnum_samplesis already capped tovocab_size // 3for entities -- nearly a third of the whole vocabulary sampled as noise every step regardless. A full softmax is exact (no noise-distribution correction term needed) and tractable at this vocab size on current GPU hardware -- the same "swap a reference-specific efficiency mechanism for a modern equivalent once its own justification no longer applies at this scale" precedent as BootEALinker'sscipy.optimize.linear_sum_assignmentsubstitution forgraph_tool/igraph.num_samplesis therefore not exposed as a constructor parameter. - No disk-cached path table. OpenEA caches sampled paths to
{data_path}/paths_{alpha}_{beta}and reloads if present. This repo'sfit()takes in-memory data with no persistent working directory, so paths are always freshly sampled each call. max_pathscap.repeat_timeswalks per (already-expanded) row of the augmented KB can produce a large path table at real dataset scale. An optionalmax_pathscap (random subsample, fixed for the run) is exposed as a pragmatic safety valve -- same precedent as IPTransELinker'sbootstrap_pool_size. DefaultNone(unbounded, matching OpenEA).
RSN4EALinker ¶
RSN4EALinker(
hidden_size: int = 100,
num_layers: int = 2,
keep_prob: float = 0.6,
max_length: int = 15,
alpha: float = 0.7,
beta: float = 0.7,
repeat_times: int = 2,
max_paths: int | None = None,
num_epochs: int = 30,
batch_size: int = 512,
learning_rate: float = 0.0005,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a Recurrent Skipping Network over sampled walk paths.
Must be fit before link can be called. Unlike MTransELinker/ SEALinker, scoring uses raw (unprojected) embeddings on both sides -- RSN4EA never builds a mapping matrix; see the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hidden_size
|
int
|
Entity/relation embedding dimensionality and LSTM
hidden size. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_layers
|
int
|
LSTM depth. OpenEA's published value is |
2
|
keep_prob
|
float
|
Per-LSTM-layer output dropout keep-probability. OpenEA's
published value is |
0.6
|
max_length
|
int
|
Sampled path length -- must be odd and >= 3 (entities
at even positions, relations at odd positions). OpenEA's
published value is |
15
|
alpha
|
float
|
Path-sampling depth-bias weight (down-weights continuations
already directly reachable from a walk's own start entity).
OpenEA's published value is |
0.7
|
beta
|
float
|
Path-sampling cross-KG bias weight (up-weights continuations
whose tail is a seed-pair entity). OpenEA's published value is
|
0.7
|
repeat_times
|
int
|
Walks sampled per row of the augmented knowledge
base. OpenEA's published value is |
2
|
max_paths
|
int | None
|
Optional cap on the number of sampled paths kept for
training (random subsample, fixed for the run) -- see the
module docstring. |
None
|
num_epochs
|
int
|
Training epochs. OpenEA's published value is |
30
|
batch_size
|
int
|
Mini-batch size for path training. OpenEA's published
value is |
512
|
learning_rate
|
float
|
Adam's learning rate (see the module docstring for
why Adam, not the config's stated Adagrad). OpenEA's published
value is |
0.0005
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> RSN4EALinker
Sample walk paths over an alias-expanded graph and train the RSN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. Unused beyond mirroring sibling
linkers' signatures -- which entities get embeddings is
determined entirely by |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity
ids on both sides must already be disjoint, as they are
from a
GraphDatasetLoader's
|
required |
random_state
|
int | None
|
Seed for reproducible path sampling and training. Left unspecified, both are non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
RSN4EALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
373 374 375 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/rsn4ea.py
377 378 379 | |
SEA¶
sea ¶
SEA knowledge-graph-embedding linker for Entity Alignment.
Pei, S., Yu, L., Hoehndorf, R., & Zhang, X. (2019). Semi-Supervised Entity Alignment via Knowledge Graph Embedding with Awareness of Degree Difference. WWW 2019. https://dl.acm.org/citation.cfm?id=3313646
Like MTransELinker, this is
a faithful reimplementation of SEA's actual training procedure, ported
directly from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/sea.py,
plus its models/basic_model.py and modules/train/batch.py support
code) rather than from a from-scratch reading of the paper.
SEA trains a single shared entity/relation embedding table with
disjoint per-KG id ranges (OpenEA's alignment_module="mapping", the
same shape MTransELinker
uses -- unlike IPTransELinker's
id-sharing or BootEALinker's
pseudo-triple-injecting "swapping"), via standard margin-based TransE with
uniform per-KG negative sampling -- no path loss, no bootstrapping, the
simplest structural half in this family after MTransE. What makes SEA
distinct is its mapping half:
- Two orthogonally-initialized square mapping matrices,
mat_1(KG1 -> KG2) andmat_2(KG2 -> KG1) -- not MTransE's single matrix. - A supervised term over seed pairs:
mat_1-mapped source entities are pulled toward their real target (and symmetricallymat_2-mapped targets toward their real source). - A semi-supervised (cycle-consistency) term over unlabeled entities:
a KG1 entity mapped through
mat_1then back throughmat_2should return to itself (and symmetrically for KG2) -- this needs no ground-truth pairing at all, just entity ids from both sides, which is what "semi-supervised" refers to in the method's name.
Ported into a private helper module, _sea_torch.py (the training-step
functions that build/consume PyTorch tensors; negative sampling itself
reuses _iptranse_training.py's sample_negative_triples directly,
since it's already fully generic).
Deliberately not ported: OpenEA's sea.py defines eye_mat_1/
eye_mat_2 (the same orthogonality-regularizer constants
MTransELinker's mapping
loss uses) but never references them in SEA's actual mapping loss -- no
orthogonality regularization is applied here, unlike MTransE. Same "port
actual, not apparent, reference behavior" situation as
BootEALinker's skipped
likelihood() and JAPELinker's
inert sim_optimizer (#28) -- skipped here entirely.
Deviations from OpenEA's own code:
var_list=Nonegotcha, checked and applicable: like MTransELinker/ KDCoELinker (seefeedback_openea_var_list_none), OpenEA'sgenerate_optimizer(mapping_loss, lr, opt='Adam')leavesvar_listunset, so TensorFlow differentiates the mapping loss against every trainable variable it touches -- both mapping matrices and the shared entity embeddings (every term reads fromself.ent_embeds). This port's mapping optimizer is scoped to[mapping_mat_1, mapping_mat_2, entity_embeds], not just "the two obvious" mapping matrices.- Optimizer is Adam, not Adagrad -- unlike MTransE/IPTransE/BootEA.
OpenEA's own
SEA.init()assertsself.args.optimizer == 'Adam'and its publishedsea_args_15K.jsonconfirms it. - Unlabeled/semi-supervised pool: OpenEA samples from its own loader's
test_links + valid_links(using only entity ids, never the gold pairing). This repo'sfit()only receivesdataset1/dataset2(full entity rosters) andground_truth(the train split), so the pool here is every entity not already aground_truthsource/ target, sampled independently per side -- the same broader-pool deviation already established for IPTransELinker's and BootEALinker's bootstrap pools, and in practice covers the same entities OpenEA's own pool would (val+test). - KG1/KG2 triple partition: like
IPTransELinker, this
repo takes one combined
graphinfit()rather than OpenEA's own separately-loaded per-KGKGs; a triple belongs to KG1 iff its head id is one ofdataset1's ids, relying on the same disjoint-id assumptionMTransELinkeralready documents.
SEALinker ¶
SEALinker(
embedding_dim: int = 100,
num_epochs: int = 500,
batch_size: int = 5000,
learning_rate: float = 0.01,
margin: float = 1.5,
alpha_1: float = 2.5,
alpha_2: float = 0.25,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via structural TransE plus a semi-supervised dual mapping.
Must be fit before link can be called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings and both (square) mapping matrices. OpenEA's
published EN-FR-15K-V1 config uses |
100
|
num_epochs
|
int
|
Training epochs, each alternating one epoch of
structural triple-loss training with one epoch of mapping-loss
training. OpenEA's config allows up to |
500
|
batch_size
|
int
|
Mini-batch size for structural triple-loss training, split proportionally between the two KGs by triple count each epoch. The mapping loss reuses the resulting step count. |
5000
|
learning_rate
|
float
|
Adam's learning rate, for both the structural and
mapping optimizers -- OpenEA's published value is |
0.01
|
margin
|
float
|
Hinge margin for the structural triple loss. OpenEA's
published value is |
1.5
|
alpha_1
|
float
|
Weight applied to the supervised mapping loss (seed-pair
approximation error). OpenEA's published value is |
2.5
|
alpha_2
|
float
|
Weight applied to the semi-supervised (cycle-consistency)
mapping loss over unlabeled entities. OpenEA's published value
is |
0.25
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/sea.py
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> SEALinker
Train shared structural TransE embeddings plus a dual cross-KG mapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
SEALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/sea.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Projected via mapping_mat_1.
Source code in src/linkingtk/algorithms/ea/sea.py
406 407 408 409 410 411 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side (unprojected).
Source code in src/linkingtk/algorithms/ea/sea.py
413 414 415 | |
BootEA¶
bootea ¶
BootEA knowledge-graph-embedding linker for Entity Alignment.
Sun, Z., Hu, W., Zhang, Q., & Qu, Y. (2018). Bootstrapping Entity Alignment with Knowledge Graph Embedding. IJCAI 2018. https://www.ijcai.org/proceedings/2018/0611.pdf
Like MTransELinker and its
siblings, this is a faithful reimplementation of BootEA's actual training
procedure, ported directly from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/bootea.py,
which extends approaches/aligne.py, plus its
modules/bootstrapping/alignment_finder.py,
modules/train/batch.py, modules/load/kgs.py support code) rather
than from a from-scratch reading of the paper.
BootEA trains a single shared entity/relation embedding table
(disjoint per-KG id ranges, no id merging -- OpenEA's
alignment_module="swapping", unlike
IPTransELinker's
"sharing" id-merge and unlike
MTransELinker's learned
mapping matrix) with the "limited" loss -- a double-margin hinge
(pos_loss = relu(pos_score - pos_margin),
neg_loss = relu(neg_margin - neg_score)), not the single-hinge margin
loss MTransE/IPTransE use. Three mechanisms distinguish it from a plain
structural baseline:
- Seed pairs are baked into the main structural triple set from the
start, not fed through a separate loss channel: for each seed pair
(a, b), every real KG1 triple touchingabecomes an extra pseudo-triple withareplaced byb(and symmetrically for KG2), folded directly into each side's own triple set (OpenEA'sgenerate_sup_relation_triples, called once atKGs-construction time) -- so the main loss pullsa's andb's embeddings together implicitly via shared structural neighbors, no mapping matrix needed. - Truncated (hard) negative sampling: each triple's corrupted
head/tail is drawn from that entity's current K-nearest-neighbor set by
embedding similarity (only the top
1 - truncated_epsilonfraction of same-KG entities are eligible), not uniformly -- refreshed every outer iteration from the live embeddings. - Bootstrapping with two-sided editing + max-weight bipartite
matching, run once per outer iteration (every
sub_epochepochs): candidate pairs are filtered by similarity threshold and row-wise top-k, then resolved into a true 1-to-1 assignment via maximum- weight bipartite matching (see _bootea_training for thescipy-based substitution for OpenEA'sgraph_tool/igraph), then merged into an accumulated labeled-alignment set with editing (a later, higher-confidence match can overwrite an earlier one). The entire current labeled-alignment set -- not just this round's deltas -- is turned into pseudo-triples the same way seed pairs are, and trained for exactly 1 epoch through a separate alignment loss/optimizer (a positive-only sigmoid loss, no negative sampling -- a different shape than the main limited loss).
Deliberately not ported: OpenEA's bootea.py also defines a
likelihood() EM-style step (_define_likelihood_graph), but
run()'s call to it is commented out
(# self.likelihood(labeled_align)) -- the reference never actually
runs it. Same "port the actual, not apparent, reference behavior"
situation as JAPELinker's
sim_optimizer (#28) -- skipped here entirely.
Ported into two private helper modules:
_bootea_training.py (plain numpy, independently testable without
torch) and _bootea_torch.py (the training-step functions that
build/consume PyTorch tensors).
Deviations from OpenEA's own code, beyond what those two modules already document:
- Bootstrap reference pool: OpenEA restricts the "not yet aligned"
search pool to its own dataset loader's
valid_entities + test_entities. This repo'sfit()only receivesdataset1/dataset2andground_truth(the train split), so the pool here is every entity not already aground_truthsource/target -- broader than OpenEA's pool, same already-established deviation as IPTransELinker's bootstrap pool and JAPELinker's attribute reference pool (reuses_jape_training.py'sreference_poolsdirectly). - Max-weight bipartite matching via
scipy.optimize.linear_sum_assignmentinstead of OpenEA'sgraph_tool/igraph-- see_bootea_training.py'sfind_mwgm_pairsdocstring. var_list=Nonegotcha checked, not applicable: unlike MTransE/KDCoE, BootEA has no separate mapping-matrix parameter to under-scope an optimizer against --ent_embeds/rel_embedsare the only trainable variables in scope for every optimizer this method defines, so leaving an optimizer's variable list unset already resolves to exactly the intended target.
BootEALinker ¶
BootEALinker(
embedding_dim: int = 100,
num_epochs: int = 2000,
sub_epoch: int = 10,
batch_size: int = 5000,
learning_rate: float = 0.01,
pos_margin: float = 0.01,
neg_margin: float = 2.0,
neg_margin_balance: float = 0.2,
neg_triple_num: int = 10,
truncated_epsilon: float = 0.9,
sim_th: float = 0.7,
k: int = 10,
bootstrap_pool_size: int | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a shared TransE space, grown by editing-based bootstrapping.
Must be fit before link can be called. See the module docstring for how this differs from MTransELinker and IPTransELinker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the trained entity/relation
embeddings. OpenEA's published EN-FR-15K-V1 config uses
|
100
|
num_epochs
|
int
|
Total structural-training-epoch cap, split across
outer bootstrapping iterations of |
2000
|
sub_epoch
|
int
|
Epochs per outer iteration, between bootstrapping
rounds. OpenEA's published value is |
10
|
batch_size
|
int
|
Mini-batch size for both the structural and alignment
losses. OpenEA's published value is |
5000
|
learning_rate
|
float
|
Adagrad's learning rate, for both optimizers.
OpenEA's published value is |
0.01
|
pos_margin
|
float
|
Positive-triple margin in the "limited" loss --
positive scores aren't penalized until they exceed this.
OpenEA's published value is |
0.01
|
neg_margin
|
float
|
Negative-triple margin -- negative scores aren't
penalized until they drop below this. OpenEA's published
value is |
2.0
|
neg_margin_balance
|
float
|
Weight applied to the negative-triple loss
term relative to the positive term. OpenEA's published value
is |
0.2
|
neg_triple_num
|
int
|
Negative triples sampled per positive. OpenEA's
published value is |
10
|
truncated_epsilon
|
float
|
Fraction of each KG's entities excluded from
an entity's truncated negative-sampling candidate set (only
the top |
0.9
|
sim_th
|
float
|
Minimum similarity for a bootstrap round's candidate pair.
OpenEA's published value is |
0.7
|
k
|
int
|
Max candidate matches kept per reference-pool row before
max-weight bipartite matching. OpenEA's published value is
|
10
|
bootstrap_pool_size
|
int | None
|
Cap on each side's bootstrap reference pool
size (random subsample, fixed for the whole run) -- same
deviation-mitigation as
IPTransELinker's
own parameter: this repo's broader-than-OpenEA reference pool
(see the module docstring) means |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/bootea.py
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> BootEALinker
Train a shared TransE space with seed pseudo-triples and iterative bootstrapping.
See the module docstring for the three mechanisms that distinguish this from a plain structural baseline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs -- entity ids on both sides must already be disjoint. |
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
BootEALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/bootea.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/bootea.py
499 500 501 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/bootea.py
503 504 505 | |
_bootea_training ¶
Private, plain-numpy (torch-free) helpers for BootEALinker.
Ported from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA -- approaches/bootea.py,
modules/bootstrapping/alignment_finder.py). Independently testable
without torch installed -- see _bootea_torch.py for the
loss/training-step functions.
compute_truncated_neighbors ¶
compute_truncated_neighbors(
entity_embeds: NDArray[floating[Any]],
entity_ids: NDArray[int64],
k: int,
) -> dict[int, npt.NDArray[np.int64]]
Each of entity_ids' own top-k nearest neighbors, restricted to entity_ids itself.
Ports find_neighbours: similarity is a plain dot product (callers
pass already L2-normalized embeddings), restricted to one KG's own
entities (entity_ids) so negative-sampling replacements never
cross the KG boundary. Matches the reference in not excluding an
entity from its own neighbor set (self-similarity is always highest,
so an entity typically appears as its own top neighbor) --
harmless downstream, since
sample_truncated_negative_triples's
real-triple check just retries if a self-corruption reproduces the
positive triple.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_embeds
|
NDArray[floating[Any]]
|
Full |
required |
entity_ids
|
NDArray[int64]
|
This KG's own entity ids (a subset of
|
required |
k
|
int
|
Number of neighbors to keep per entity (OpenEA's
|
required |
Returns:
| Type | Description |
|---|---|
dict[int, NDArray[int64]]
|
|
dict[int, NDArray[int64]]
|
entry per id in |
dict[int, NDArray[int64]]
|
empty. |
Source code in src/linkingtk/algorithms/ea/_bootea_training.py
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | |
edit_labeled_alignment ¶
edit_labeled_alignment(
pre_alignment: dict[int, int],
curr_alignment: set[tuple[int, int]],
sim_mat: NDArray[floating[Any]],
) -> dict[int, int]
Merge this round's matched pairs into the accumulated labeled alignment, with editing.
Ports update_labeled_alignment_x (composed with)
update_labeled_alignment_y:
- For each
(i, j)freshly matched this round: ifialready has an earlier label, replace it only if the new pair's similarity is>=the old label's similarity (walks back an earlier mistaken label when a later round finds better evidence); otherwise it's a brand-new label. - Resolve multiple sources now claiming the same target: keep only the claimant with the highest similarity to that target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pre_alignment
|
dict[int, int]
|
The accumulated |
required |
curr_alignment
|
set[tuple[int, int]]
|
This round's freshly matched |
required |
sim_mat
|
NDArray[floating[Any]]
|
The same similarity matrix |
required |
Returns:
| Type | Description |
|---|---|
dict[int, int]
|
The updated, edited |
Source code in src/linkingtk/algorithms/ea/_bootea_training.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | |
encode_triples ¶
encode_triples(
triples: NDArray[int64],
) -> npt.NDArray[np.int64]
Pack (head, relation, tail) id rows into single int64 keys for fast set membership.
(h * _ID_ENCODE_SHIFT + r) * _ID_ENCODE_SHIFT + t -- safe for id
spaces up to a million entities/relations each (int64 easily
covers the ~10^18 result), and lets triple-membership checks use
np.isin (vectorized, sort-based) instead of a Python set of
tuples, which the real-triple-avoidance check in
sample_truncated_negative_triples
calls often enough (BootEA's neg_triple_num=10, an order of
magnitude more than every other linker) for the pure-Python
tuple-construction-plus-set-lookup cost to dominate real-dataset-scale
training wall-clock time (confirmed via profiling).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
|
required |
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
Source code in src/linkingtk/algorithms/ea/_bootea_training.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
find_mwgm_pairs ¶
find_mwgm_pairs(
sim_mat: NDArray[floating[Any]], sim_th: float, k: int
) -> list[tuple[int, int, float]]
Candidate filtering (threshold ∩ row-wise top-k) then maximum-weight bipartite matching.
Ports find_potential_alignment_mwgm/find_alignment: a pair
(i, j) is a candidate iff sim[i, j] > sim_th and j is
among row i's k largest entries (unlike
find_new_pairs's
plain row-argmax, this keeps up to k candidates per row, not just
the single best). Candidates then go through maximum-weight
bipartite matching, via scipy.optimize.linear_sum_assignment
(already a dependency) rather than OpenEA's graph_tool/igraph
-- a deliberate, documented dependency substitution (same rationale as
IMUSE #32's rapidfuzz): build a dense cost submatrix over just the
distinct row/col indices appearing in the candidate set (non-candidate
cells get a large sentinel cost so they're only chosen if the
assignment is forced to), solve it, then drop any returned pair that
wasn't an actual candidate -- this gives the same "true 1-to-1
assignment, greedy pairs discarded if a better global assignment
exists" property without the extra dependency.
Uses a plain per-row argsort for the top-k step rather than
OpenEA's argpartition, for readability and to sidestep
argpartition's "kth must be less than array size" edge case on
small matrices -- same simplification precedent as
sample_negative_triples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sim_mat
|
NDArray[floating[Any]]
|
|
required |
sim_th
|
float
|
Minimum similarity for a pair to be a candidate at all. |
required |
k
|
int
|
Max candidates kept per row (OpenEA's published value is
|
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, int, float]]
|
|
list[tuple[int, int, float]]
|
pairs. Empty if |
list[tuple[int, int, float]]
|
|
Source code in src/linkingtk/algorithms/ea/_bootea_training.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
pseudo_triples_for_pairs ¶
pseudo_triples_for_pairs(
pairs: list[tuple[int, int]],
by_head1: dict[int, list[tuple[int, int]]],
by_tail1: dict[int, list[tuple[int, int]]],
by_head2: dict[int, list[tuple[int, int]]],
by_tail2: dict[int, list[tuple[int, int]]],
) -> tuple[
list[tuple[int, int, int]], list[tuple[int, int, int]]
]
Unweighted pseudo-triples for a batch of aligned pairs, split by originating KG.
Ports generate_supervised_triples/generate_newly_triples: for
each (source, target) pair, every real KG1 edge touching source
becomes an extra KG1-side triple with source replaced by
target (and symmetrically, every real KG2 edge touching target
becomes an extra KG2-side triple with target replaced by
source). Reuses
pseudo_triples_for_pair
for the actual substitution (called with weight=1.0 and the weight
discarded) -- unlike IPTransE's own bootstrap pseudo-triples, BootEA's
reference treats every pseudo-triple equally, with no
similarity-derived weight.
This is used both for seed pairs (baked into the main structural
triple set once, at fit() start) and for the bootstrap-found
labeled-alignment set (retrained fresh via the separate alignment loss
every outer iteration) -- see
BootEALinker's module
docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pairs
|
list[tuple[int, int]]
|
|
required |
by_head1
|
dict[int, list[tuple[int, int]]]
|
KG1's own |
required |
by_tail1
|
dict[int, list[tuple[int, int]]]
|
KG1's own |
required |
by_head2
|
dict[int, list[tuple[int, int]]]
|
KG2's own |
required |
by_tail2
|
dict[int, list[tuple[int, int]]]
|
KG2's own |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, int, int]]
|
|
list[tuple[int, int, int]]
|
|
tuple[list[tuple[int, int, int]], list[tuple[int, int, int]]]
|
ids in one triple (the substituted endpoint comes from the other |
tuple[list[tuple[int, int, int]], list[tuple[int, int, int]]]
|
side) -- this is deliberate, matching OpenEA's own behavior of |
tuple[list[tuple[int, int, int]], list[tuple[int, int, int]]]
|
folding them into each side's own triple set regardless. |
Source code in src/linkingtk/algorithms/ea/_bootea_training.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
sample_truncated_negative_triples ¶
sample_truncated_negative_triples(
positive_triples: NDArray[int64],
neighbor_candidates: dict[int, NDArray[int64]],
fallback_pool: NDArray[int64],
real_triples: NDArray[int64],
rng: Generator,
max_tries: int = 10,
) -> npt.NDArray[np.int64]
Like sample_negative_triples but corrupted from each entity's own truncated neighbor set.
Ports generate_neg_triples_fast's neighbor-restricted
candidate behavior: the replacement head/tail is drawn from
neighbor_candidates.get(entity, fallback_pool) -- typically each
entity's current K-nearest-neighbor set by embedding similarity (see
compute_truncated_neighbors),
which produces harder negatives than uniform sampling. Falls back to
fallback_pool for an entity with no computed neighbor set yet
(e.g. before the first neighbor refresh).
Vectorized rather than a plain per-row retry loop (unlike
sample_negative_triples's
established simplification): BootEA's own published neg_triple_num
is 10 (vs. every other linker's 1), so this gets called an
order of magnitude more per training step -- a scalar-Python retry
loop measurably bottlenecks real-dataset-scale training (confirmed via
profiling on EN-FR-15K-V1: 0% GPU utilization, all wall-clock time
spent in this function). Draws are batched per unique corrupted
entity via numpy fancy-indexing; the real-triple-avoidance check uses
encode_triples
+ np.isin (vectorized) rather than a Python set of tuples, and
each retry pass only re-draws rows that actually collided (typically
few, and shrinking), not the whole array every pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positive_triples
|
NDArray[int64]
|
|
required |
neighbor_candidates
|
dict[int, NDArray[int64]]
|
|
required |
fallback_pool
|
NDArray[int64]
|
Candidate pool used when an entity has no entry in
|
required |
real_triples
|
NDArray[int64]
|
Sorted
encode_triples-encoded
keys to avoid reproducing (e.g. |
required |
rng
|
Generator
|
Random generator, for reproducibility. |
required |
max_tries
|
int
|
Retry passes over still-colliding rows before accepting the last draw regardless. |
10
|
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[int64]
|
|
Source code in src/linkingtk/algorithms/ea/_bootea_training.py
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 | |
GCN-Align¶
gcn_align ¶
GCN-Align knowledge-graph-embedding linker for Entity Alignment.
Wang, Z., Lv, Q., Lan, X., & Zhang, Y. (2018). Cross-lingual Knowledge Graph Alignment via Graph Convolutional Networks. EMNLP 2018. https://aclanthology.org/D18-1032/
The first GNN-based EA linker in this package (see #18's parent issue) --
unlike every linker in
linkingtk.algorithms.ea so far, entity representations come from
propagating embeddings through a fixed graph convolution over the combined
relational structure, not from a triple-local translational/distance loss
over embeddings directly. This is a faithful port of OpenEA's reference
implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/gcn_align.py),
not a from-scratch reading of the paper, following this repo's established
convention (e.g. MTransELinker,
RSN4EALinker).
Both of OpenEA's branches are implemented: the structural (se)
branch propagates a directly-learnable per-entity embedding table; the
attribute (ae) branch propagates a learned transform of each entity's
one-hot attribute-predicate-presence vector (see
build_attribute_features
for why predicates, not values). Both are 2-layer GCNs over the same
structural adjacency, trained independently (separate optimizers, never a
combined loss) with the same shared negatives each step, matching
OpenEA's own GCN_Align.train_embeddings. At evaluation time (and for
early-stopping/final embeddings), scores use the concatenation
[se * beta, ae * (1 - beta)] -- OpenEA's own test_method: "sa",
the config that produced the published EN-FR-15K-V1 numbers.
use_attributes=False (default) skips the ae branch entirely,
needing nothing beyond EnFr15KDataset's
plain relational triples -- pass use_attributes=True plus
EnFr15KAttrDataset's attribute
triples to fit() for numbers directly comparable to OpenEA's published
Hits@1=0.338, Hits@10=0.680, MRR=0.451
(docs/detailed_results_current_approaches_15K.csv).
Deliberate deviations, confirmed against OpenEA's own reference config
(run/args/gcnalign_args_15K.json) and source:
- Plain SGD, not Adam/Adagrad. OpenEA's own
GCN_Align_Unitusestf.train.GradientDescentOptimizerand assertsargs.learning_rate >= 0.01-- its own published value is an unusually highlearning_rate=8, which is intentional fidelity to the reference, not a typo. Every other hand-rolled linker in this package uses Adam/Adagrad; this one deliberately doesn't, to match what the reference actually runs. - No mapping matrix. OpenEA's generic pipeline config labels this
"alignment_module": "mapping", butgcn_align.py's own model code never builds or trains a projection matrix -- entities from both KGs share one embedding table (disjoint ids, same pattern as RSN4EALinker), andsource_embedding/target_embeddingare identical passthroughs. - Negative sampling refresh cadence is fixed at every 10 epochs
(matching OpenEA's
if i % 10 == 1), independent ofeval_every. - Candidate scoring uses Manhattan (L1) distance, not cosine
similarity. OpenEA's own config for this method is
eval_metric: "manhattan",eval_norm: false(confirmed by readingrun/args/gcnalign_args_15K.jsondirectly) -- and the margin loss both branches train under (margin_ranking_loss_l1) is itself L1-based, so embeddings are optimized to be close in L1 distance, not high-cosine. Scoring with a metric that matches the training geometry, rather than an arbitrary different one, is a real correctness fix, not just fidelity to OpenEA's own choice -- confirmed empirically on the real EN-FR-15K-V1 benchmark (structural-only): Hits@1 0.130 (cosine) -> 0.147 (manhattan, no CSLS). CSLS (Cross-domain Similarity Local Scaling,csls_k=10in OpenEA's own config) is a further, separate refinement -- see rank_exhaustive'scsls_kparameter, used byexamples/gcn_align_benchmark.pybut not bylink()itself (CSLS needs the full candidate pool's neighbor structure, which doesn't fitlink()'s post-blocking, per-source candidate-list shape).
GCNAlignLinker ¶
GCNAlignLinker(
embedding_dim: int = 100,
num_epochs: int = 500,
learning_rate: float = 8.0,
neg_triple_num: int = 5,
gamma: float = 3.0,
min_weight: float = 0.3,
use_attributes: bool = False,
attr_dim: int = 100,
attr_top_fraction: float = 0.7,
beta: float = 0.9,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via a 2-layer GCN's propagated embeddings.
Must be fit before
link can be called. See the
module docstring for what's ported vs. deliberately deviated from OpenEA,
and for the structural-only-vs.-sa (structural+attribute) tradeoff.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Dimensionality of the propagated structural
embeddings (OpenEA's |
100
|
num_epochs
|
int
|
Training epochs. OpenEA's published value allows up to
|
500
|
learning_rate
|
float
|
Plain SGD's learning rate -- see the module docstring
for why this is unusually high by this package's conventions.
OpenEA's published value is |
8.0
|
neg_triple_num
|
int
|
Negatives sampled per seed pair per side ( |
5
|
gamma
|
float
|
Margin for
margin_ranking_loss_l1.
OpenEA's published value is |
3.0
|
min_weight
|
float
|
Floor applied to every adjacency edge weight -- see
build_weighted_adjacency.
OpenEA's own value (hardcoded, not configurable in the
reference) is |
0.3
|
use_attributes
|
bool
|
Whether to also train the attribute ( |
False
|
attr_dim
|
int
|
Dimensionality of the propagated attribute embeddings
(OpenEA's |
100
|
attr_top_fraction
|
float
|
Fraction of the combined attribute-predicate
vocabulary kept as feature columns -- see
build_attribute_features.
Only used if |
0.7
|
beta
|
float
|
Weight on the structural half of the concatenated
|
0.9
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/gcn_align.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
attribute_triples1: list[Triple] | None = None,
attribute_triples2: list[Triple] | None = None,
) -> GCNAlignLinker
Propagate structural (and, if enabled, attribute) embeddings through the graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. Unused beyond mirroring sibling
linkers' signatures -- which entities get embeddings is
determined entirely by |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
attribute_triples1
|
list[Triple] | None
|
KG1's |
None
|
attribute_triples2
|
list[Triple] | None
|
KG2's own attribute triples. See
|
None
|
Returns:
| Type | Description |
|---|---|
GCNAlignLinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/gcn_align.py
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/gcn_align.py
441 442 443 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/gcn_align.py
445 446 447 | |
_gcn_align_training ¶
Private, plain-numpy (torch-free) helpers for GCNAlignLinker.
Ported from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA -- approaches/gcn_align.py's
GCN_Utils.func/ifunc/get_weighted_adj,
GCN_Align.train_embeddings's uniform negative-sampling arrays, and
load_attr for the attribute-presence feature matrix). Independently
testable without torch installed -- see _gcn_align_torch.py for
the model/training-step functions.
build_attribute_features ¶
build_attribute_features(
attribute_triples1: list[Triple],
attribute_triples2: list[Triple],
entity_to_id: dict[str, int],
top_fraction: float = _DEFAULT_TOP_FRACTION,
) -> tuple[
npt.NDArray[np.int64], npt.NDArray[np.float64], int
]
One-hot attribute-predicate-presence features, per OpenEA's load_attr.
Ports load_attr exactly: the feature vocabulary is the top
top_fraction (by how many distinct entities carry each predicate,
combined across both KGs) attribute predicates -- not values, since
load_attr reads its input from entity_attributes_dict (entity
-> set of predicate URIs that entity has an attribute triple for),
discarding literal values entirely. An entity gets feature 1.0 at
a predicate's column iff it has at least one attribute triple with
that predicate; the specific value(s) never matter. Predicates outside
the top top_fraction are dropped (contribute no feature column at
all, not folded into an "other" bucket), matching OpenEA's own
if v in attr2id guard.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attribute_triples1
|
list[Triple]
|
KG1's |
required |
attribute_triples2
|
list[Triple]
|
KG2's own attribute triples. |
required |
entity_to_id
|
dict[str, int]
|
Combined entity label -> id mapping, e.g. from build_id_mappings. Triples whose entity id isn't a key here are dropped. |
required |
top_fraction
|
float
|
Fraction of the combined, frequency-ranked predicate
vocabulary to keep as feature columns. OpenEA's own value
(hardcoded, not configurable in the reference) is |
_DEFAULT_TOP_FRACTION
|
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[float64]
|
int64 (row 0 = entity id, row 1 = attribute-column id -- ready for |
int
|
|
tuple[NDArray[int64], NDArray[float64], int]
|
use directly via |
tuple[NDArray[int64], NDArray[float64], int]
|
|
tuple[NDArray[int64], NDArray[float64], int]
|
with |
tuple[NDArray[int64], NDArray[float64], int]
|
|
tuple[NDArray[int64], NDArray[float64], int]
|
feature vocabulary size ( |
tuple[NDArray[int64], NDArray[float64], int]
|
at all). |
Source code in src/linkingtk/algorithms/ea/_gcn_align_training.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
build_weighted_adjacency ¶
build_weighted_adjacency(
triples: NDArray[int64],
r2f: dict[int, float],
r2if: dict[int, float],
min_weight: float = _DEFAULT_MIN_WEIGHT,
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]
Directed, relation-functionality-weighted adjacency, per OpenEA's get_weighted_adj.
For every triple (h, r, t) with h != t: edge h -> t gets
weight max(r2if[r], min_weight) and edge t -> h gets weight
max(r2f[r], min_weight) -- edges through more "selective"/functional
relations carry more weight. Weights from multiple triples sharing the
same (h, t) pair are summed. Self-loop triples (h == t) are
skipped entirely (no edge contributed either direction), matching
OpenEA's own if tri[0] == tri[2]: continue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
|
required |
r2f
|
dict[int, float]
|
Per-relation functionality, from compute_relation_functionality. |
required |
r2if
|
dict[int, float]
|
Per-relation inverse functionality, same source. |
required |
min_weight
|
float
|
Floor applied to every edge weight. |
_DEFAULT_MIN_WEIGHT
|
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[float64]
|
source/row id, row 1 = target/column id -- ready for |
tuple[NDArray[int64], NDArray[float64]]
|
|
tuple[NDArray[int64], NDArray[float64]]
|
|
tuple[NDArray[int64], NDArray[float64]]
|
if every triple is a self-loop or |
Source code in src/linkingtk/algorithms/ea/_gcn_align_training.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
compute_relation_functionality ¶
compute_relation_functionality(
triples: NDArray[int64],
) -> tuple[dict[int, float], dict[int, float]]
Per-relation functionality/inverse-functionality, per OpenEA's func/ifunc.
r2f[r] = (# distinct head entities of r) / (# triples with
r) -- how "selective" r is as a function of its head.
r2if[r] is the same but over tail entities. Both are used by
build_weighted_adjacency
to weight each triple's structural-influence edges.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
|
required |
Returns:
| Type | Description |
|---|---|
dict[int, float]
|
|
dict[int, float]
|
|
Source code in src/linkingtk/algorithms/ea/_gcn_align_training.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | |
sample_negatives ¶
sample_negatives(
seed_pairs: list[tuple[int, int]],
num_entities: int,
neg_triple_num: int,
rng: Generator,
) -> tuple[
npt.NDArray[np.int64],
npt.NDArray[np.int64],
npt.NDArray[np.int64],
npt.NDArray[np.int64],
]
Two-sided uniform negative sampling, per OpenEA's train_embeddings setup.
For t seed pairs and k = neg_triple_num negatives per pair:
right-corruption keeps each pair's own left id fixed (repeated k
times) and draws k random right ids; left-corruption is the mirror
image. Both id sides are drawn from the same combined [0,
num_entities) id space -- GCN-Align trains one shared embedding table
over both KGs' entities, not two independently-indexed spaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seed_pairs
|
list[tuple[int, int]]
|
|
required |
num_entities
|
int
|
Total entity count (both KGs combined) to sample negatives from. |
required |
neg_triple_num
|
int
|
Negatives per pair per side ( |
required |
rng
|
Generator
|
Source of randomness. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[int64]
|
int64 -- pass directly to |
NDArray[int64]
|
Source code in src/linkingtk/algorithms/ea/_gcn_align_training.py
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
_gcn_align_torch ¶
PyTorch model and training-step functions for GCNAlignLinker.
Ports OpenEA's GCN_Align_Unit/GraphConvolution (approaches/gcn_align.py)
both branches:
- The structural (
se) branch: a 2-layer, purely-propagation GCN over a directly-learnable per-entity embedding table -- layer 1 is "featureless" (its only weight is the learnable embedding table itself, transformed by nothing but the adjacency), layer 2 has no weight at all (transform=False,input_dim == output_dim), so the whole branch isA_norm @ relu(A_norm @ E)for a learnableE. - The attribute (
ae) branch: the same 2-layer shape, but layer 1 has a real learnable weight matrix transforming the sparse attribute-presence input (from build_attribute_features) before propagating --A_norm @ relu(A_norm @ (attrs @ W)).
Uses plain torch.sparse.mm throughout (see
coo_to_torch_sparse) rather
than a GNN library -- see the module docstring on
gcn_align for why.
Callers must already have confirmed torch is importable
(gcn_align.py's fit() does this via OptionalDependencyError
before any of these run) -- same precedent as _rsn4ea_torch.py/
_kdcoe_torch.py.
build_gcn_align_attr_branch ¶
build_gcn_align_attr_branch(
num_attrs: int, dim: int
) -> torch.nn.Module
OpenEA's GCN_Align_Unit attribute branch: a real layer-1 weight over sparse features.
Same factory-function precedent as
build_gcn_align_branch.
Layer 1's weight uses the same trunc_normal init (truncated normal,
stddev 1/sqrt(input_dim), then L2-row-normalized) OpenEA's own
GCN_Align_Unit._build applies regardless of branch (init=trunc_normal
is passed for both the se and ae branches' layer 1, confirmed by
reading _build directly -- only featureless/sparse_inputs
differ between the two branches' constructor args).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_attrs
|
int
|
Attribute feature-vocabulary size (layer 1's input dimension), e.g. from build_attribute_features. |
required |
dim
|
int
|
Output dimensionality (OpenEA's |
required |
Returns:
| Type | Description |
|---|---|
Module
|
A module whose |
Module
|
coalesced sparse |
Module
|
adjacency and a sparse |
Module
|
feature matrix, returning |
Module
|
embeddings. |
Source code in src/linkingtk/algorithms/ea/_gcn_align_torch.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | |
build_gcn_align_branch ¶
build_gcn_align_branch(
num_entities: int, dim: int
) -> torch.nn.Module
OpenEA's GCN_Align_Unit structural branch: two featureless-first-layer GCN steps.
A factory function rather than a module-level torch.nn.Module
subclass, since torch.nn.Module can't be named at module scope
without torch installed -- same precedent as
_kdcoe_torch.build_description_encoder/_rsn4ea_torch.build_rsn_model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_entities
|
int
|
Total entity count (both KGs combined) -- both the
embedding table's row count and (per OpenEA's |
required |
dim
|
int
|
Embedding/output dimensionality (OpenEA's |
required |
Returns:
| Type | Description |
|---|---|
Module
|
A module whose |
Module
|
|
Module
|
|
Module
|
and returns |
Source code in src/linkingtk/algorithms/ea/_gcn_align_torch.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
_ea_losses ¶
Shared margin-ranking loss functions for the GNN-based EA linkers.
Both losses below are ported once from OpenEA's reference implementation and
reused across linkers, rather than each one reimplementing the same math in
its own private _<name>_torch.py:
margin_ranking_loss_l1: the L1-distance, double-sided-negative margin loss GCN-Align'salign_loss(approaches/gcn_align.py) and RDGCN'sget_loss(approaches/rdgcn.py) both use, verbatim -- confirmed by reading both directly, the two functions are identical modulo variable names.margin_ranking_loss_l2_squared: AliNet's squared-Euclidean single-sided variant (AliNet.compute_loss,approaches/alinet.py), with its ownneg_margin_balanceweight on the negative term.
Both take an already-built embedding tensor plus index tensors into it (rather than pre-gathered vectors) so callers can pass whichever entity representation is currently live (e.g. AliNet's per-round concatenated multi-layer output) without an extra gather step at the call site.
Callers must already have confirmed torch is importable (each linker's
fit() does this via OptionalDependencyError before any of these
run) -- same precedent as _rsn4ea_torch.py/_kdcoe_torch.py.
margin_ranking_loss_l1 ¶
margin_ranking_loss_l1(
embeddings: Tensor,
pos_left: Tensor,
pos_right: Tensor,
neg_left: Tensor,
neg_right: Tensor,
neg2_left: Tensor,
neg2_right: Tensor,
gamma: float,
) -> torch.Tensor
L1-distance margin loss over t positive pairs and k negatives per side.
Ports OpenEA's align_loss/get_loss exactly: for each positive
pair, both a left-corrupted (neg_left/neg_right) and a
right-corrupted (neg2_left/neg2_right) set of k negatives
contribute a relu(margin + pos_dist - neg_dist) term, averaged over
2 * k * t.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
Tensor
|
|
required |
pos_left
|
Tensor
|
|
required |
pos_right
|
Tensor
|
|
required |
neg_left
|
Tensor
|
|
required |
neg_right
|
Tensor
|
|
required |
neg2_left
|
Tensor
|
|
required |
neg2_right
|
Tensor
|
|
required |
gamma
|
float
|
Margin. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
A scalar loss tensor. |
Source code in src/linkingtk/algorithms/ea/_ea_losses.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
margin_ranking_loss_l2_squared ¶
margin_ranking_loss_l2_squared(
embeddings: Tensor,
pos_left: Tensor,
pos_right: Tensor,
neg_left: Tensor,
neg_right: Tensor,
margin: float,
neg_margin_balance: float,
) -> torch.Tensor
Squared-Euclidean margin loss: pulls positives together, pushes negatives apart.
Ports AliNet's compute_loss exactly: sum(||e1 - e2||^2) over
positive pairs, plus neg_margin_balance * sum(relu(margin -
||e1 - e2||^2)) over negative pairs (a single corruption side, unlike
margin_ranking_loss_l1's two).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
Tensor
|
|
required |
pos_left
|
Tensor
|
|
required |
pos_right
|
Tensor
|
|
required |
neg_left
|
Tensor
|
|
required |
neg_right
|
Tensor
|
|
required |
margin
|
float
|
Margin subtracted from each negative pair's squared distance. |
required |
neg_margin_balance
|
float
|
Weight applied to the summed negative-pair loss. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
A scalar loss tensor. |
Source code in src/linkingtk/algorithms/ea/_ea_losses.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
RDGCN¶
rdgcn ¶
RDGCN knowledge-graph-embedding linker for Entity Alignment.
Wu, Y., Liu, X., Feng, Y., Wang, Z., Yan, R., & Zhao, D. (2019). Relation-Aware Entity Alignment for Heterogeneous Knowledge Graphs. IJCAI 2019. https://www.ijcai.org/proceedings/2019/0733.pdf
The second GNN-based EA linker in this package (see #18's parent issue and
42's GCNAlignLinker).¶
RDGCN builds entity representations from two graphs jointly: the
"primal" graph over entities (structural, like GCN-Align's), and a "dual"
graph over relations (two relations are dual-graph-adjacent if their
head-/tail-entity sets overlap a lot), attending back and forth between
them for two rounds before a final 2-layer diagonal-weight GCN with highway
gates. This is a faithful port of OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/rdgcn.py),
not a from-scratch reading of the paper -- see
_rdgcn_training and
_rdgcn_torch for the algorithm's
own docstrings, and this module's deviations below (confirmed against
OpenEA's own reference config, run/args/rdgcn_args_15K.json, and
source):
- Requires EnFr15KAttrDataset,
not EnFr15KDataset. RDGCN's entity
representations are seeded from pretrained word-vector embeddings of
each entity's local name --
EnFr15KDataset's numeric-id, URI-free format has no name text to embed (same reason KDCoELinker (#29) needs it).fit()raisesLinkingTKErrorearly if no entity indataset1/dataset2has a non-empty label, as a cheap guard. - Local names come from
Entity.labels, not OpenEA's own URI-parsing. OpenEA's_get_local_name_by_name_triplehas a dataset-specific name-attribute matching step (special-cased predicate lists forD_Y/D_W) that falls through to a genericelse: name_attribute_list = {}for every other dataset, including EN-FR-15K -- meaning OpenEA's own code already just uses the entity URI's tail segment for this dataset, the same text this repo's loader already populates as the label. Confirmed a no-op deviation for this benchmark, not a fidelity-losing simplification. - Pretrained word vectors via
_kdcoe_text.load_fasttext_vectors, reusing exactly the file OpenEA's own reference hardcodes (wiki-news-300d-1M.vec.zip) and the same streaming-fetch mechanism KDCoELinker already established, rather than a separate implementation. - Hard-negative mining excludes each anchor's own id from its own
candidate pool -- OpenEA's own
get_negdoesn't, which (since an entity's distance to itself is always the global minimum) makes an anchor frequently rank as its own "hardest negative," a training signal that's structurally meaningless. See mine_hard_negatives's docstring. - Duplicate-relation primal edges are coalesce-summed, not kept as
separate softmax competitors -- a
torch.sparse.softmax-driven simplification vs. TensorFlow's uncoalesced-sparse-tensor behavior; see _SparseAttentionLayer's docstring (insidebuild_rdgcn_model). - Candidate scoring uses Manhattan (L1) distance, not cosine
similarity. OpenEA's own config for this method is
eval_metric: "manhattan",eval_norm: false(confirmed by readingrun/args/rdgcn_args_15K.jsondirectly) -- and margin_ranking_loss_l1 itself trains embeddings to be close in L1 distance, not high-cosine. This is a real correctness fix matching the training geometry, not just fidelity to OpenEA's own choice -- confirmed empirically: on the real EN-FR-15K-V1 benchmark, switching the evaluation metric alone (same trained embeddings, via rank_exhaustive'smetric/csls_k) moved Hits@1 from 0.666 (cosine) to 0.735 (manhattan+CSLS k=10, OpenEA's own config) against a published 0.755 -- closing the gap from ~88% to ~97% relative. CSLS (Cross-domain Similarity Local Scaling) is a further, separate refinement used byexamples/rdgcn_benchmark.pybut not bylink()itself (CSLS needs the full candidate pool's neighbor structure, which doesn't fitlink()'s post-blocking, per-source candidate-list shape).
RDGCNLinker ¶
RDGCNLinker(
embedding_dim: int = 300,
num_epochs: int = 500,
learning_rate: float = 0.002,
neg_triple_num: int = 125,
gamma: float = 1.0,
alpha: float = 0.1,
beta: float = 0.3,
default_name_length: int = 4,
word_embed_url: str = _FASTTEXT_URL,
cache_dir: Path | None = None,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via RDGCN's dual/primal-attention-plus-GCN embeddings.
Must be fit before
link can be called. See
the module docstring for what's ported vs. deliberately deviated from
OpenEA, and why dataset1/dataset2 must come from
EnFr15KAttrDataset rather than
EnFr15KDataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedding_dim
|
int
|
Entity-embedding dimensionality -- must equal the
pretrained word vectors' own dimensionality (no projection
layer sits between them). OpenEA's published EN-FR-15K-V1
config uses |
300
|
num_epochs
|
int
|
Training epochs. OpenEA's config allows up to |
500
|
learning_rate
|
float
|
Adam's learning rate. OpenEA's published value is
|
0.002
|
neg_triple_num
|
int
|
Hard negatives mined per seed pair per side ( |
125
|
gamma
|
float
|
Margin for
margin_ranking_loss_l1.
OpenEA's published value is |
1.0
|
alpha
|
float
|
Round-1 primal-embedding mixing weight. OpenEA's published
value is |
0.1
|
beta
|
float
|
Round-2 primal-embedding mixing weight. OpenEA's published
value is |
0.3
|
default_name_length
|
int
|
Max local-name tokens summed per entity for
the initial embedding. OpenEA's published value is |
4
|
word_embed_url
|
str
|
URL of a fastText-format |
_FASTTEXT_URL
|
cache_dir
|
Path | None
|
Passed through to |
None
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/rdgcn.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> RDGCNLinker
Train the two-round dual/primal-attention GCN over the combined graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- must carry non-empty |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
RDGCNLinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If no entity in |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/rdgcn.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/rdgcn.py
417 418 419 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/rdgcn.py
421 422 423 | |
_rdgcn_training ¶
Private, plain-numpy (torch-free) helpers for RDGCNLinker.
Ported from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA -- approaches/rdgcn.py's
get_mat/get_sparse_tensor (primal adjacency), rfunc/compute_r/
get_dual_input (relation "dual graph" and per-relation features), and
_get_desc_input/_get_local_name_by_name_triple (name-embedding
init)). Independently testable without torch installed -- see
_rdgcn_torch.py for the attention/GCN model and training-step functions.
build_dual_graph ¶
build_dual_graph(
triples: NDArray[int64],
num_relations: int,
num_entities: int,
) -> npt.NDArray[np.float64]
Dense relation-to-relation "dual graph", per OpenEA's get_dual_input.
Two relations are dual-graph-adjacent with weight jaccard(head-entity
sets) + jaccard(tail-entity sets) -- including the diagonal (i ==
j, weight 2.0, both Jaccard similarities of a set with itself).
Kept dense (not sparse COO) since the relation count is small (low
hundreds for OpenEA-scale datasets) and every pair genuinely
participates in the downstream dense-attention softmax's masking, unlike
the primal/entity-scale adjacencies elsewhere in this package.
Vectorized via head/tail membership matrices rather than OpenEA's own
O(R^2) nested-loop-over-Python-sets -- same result, just computed as
one matrix multiply per side.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
|
required |
num_relations
|
int
|
Total relation count. |
required |
num_entities
|
int
|
Total entity count (both KGs combined). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[float64]
|
|
Source code in src/linkingtk/algorithms/ea/_rdgcn_training.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
build_edge_relations ¶
build_edge_relations(
triples: NDArray[int64],
) -> tuple[
npt.NDArray[np.int64],
npt.NDArray[np.int64],
npt.NDArray[np.int64],
]
Parallel per-triple (head, tail, relation) id arrays, per OpenEA's rfunc's r_mat.
Unlike build_primal_adjacency, this is not deduplicated or symmetrized -- one row per triple, directed (head -> tail only), exactly as given. Used by the sparse attention layer to look up, for every primal edge, which relation connects it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
|
required |
Returns:
| Type | Description |
|---|---|
tuple[NDArray[int64], NDArray[int64], NDArray[int64]]
|
|
Source code in src/linkingtk/algorithms/ea/_rdgcn_training.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
build_primal_adjacency ¶
build_primal_adjacency(
triples: NDArray[int64], num_entities: int
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]
Unweighted, symmetric primal (entity) adjacency, per OpenEA's get_mat.
For every triple (h, r, t) with h != t, both h -> t and
t -> h get weight 1.0 -- a plain undirected adjacency,
deduplicated (unlike
build_weighted_adjacency,
repeated triples between the same entity pair do not accumulate
weight, matching OpenEA's own if (h, t) not in pos: pos[(h, t)] = 1
dedup-to-1 behavior). Self-loop triples (h == t) are skipped, same
as
build_weighted_adjacency.
Pass the result to
normalize_adjacency_coo
(add_self_loops=True) -- symmetric here, so its row-sum-vs-column-sum
distinction is a no-op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
|
required |
num_entities
|
int
|
Total entity count (both KGs combined). |
required |
Returns:
| Type | Description |
|---|---|
tuple[NDArray[int64], NDArray[float64]]
|
|
Source code in src/linkingtk/algorithms/ea/_rdgcn_training.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
build_relation_masks ¶
build_relation_masks(
triples: NDArray[int64], num_relations: int
) -> tuple[
tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]],
tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]],
]
Row-normalized head/tail entity-membership matrices, per OpenEA's rfunc/compute_r.
head_mask[r, e] = 1/|heads(r)| if e is ever a head of relation
r (0 otherwise); tail_mask is the mirror image over tails.
Membership is binary per (relation, entity) pair before normalizing --
an entity appearing as relation r's head in multiple triples still
only contributes once, matching OpenEA's own head_r[h][r] = 1
(assignment, not accumulation). Pre-normalizing (dividing by each
relation's head/tail count) here means the torch side's per-relation
feature computation is a single sparse matmul (head_mask @
primal_embeddings) rather than a matmul-then-divide.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
|
required |
num_relations
|
int
|
Total relation count. |
required |
Returns:
| Type | Description |
|---|---|
tuple[NDArray[int64], NDArray[float64]]
|
|
tuple[NDArray[int64], NDArray[float64]]
|
each |
tuple[tuple[NDArray[int64], NDArray[float64]], tuple[NDArray[int64], NDArray[float64]]]
|
entity id) and |
tuple[tuple[NDArray[int64], NDArray[float64]], tuple[NDArray[int64], NDArray[float64]]]
|
|
tuple[tuple[NDArray[int64], NDArray[float64]], tuple[NDArray[int64], NDArray[float64]]]
|
with |
Source code in src/linkingtk/algorithms/ea/_rdgcn_training.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
init_name_embeddings ¶
init_name_embeddings(
entities: list[Entity],
word_vectors: dict[str, NDArray[floating[Any]]],
dim: int,
default_length: int = 4,
) -> dict[str, npt.NDArray[np.float32]]
Each entity's initial embedding: summed word vectors of its first label's tokens.
Ports OpenEA's _get_desc_input's name-embedding init
(word_em[e_desc_input].sum(axis=1)): tokenize the entity's local
name, look up up to default_length
tokens' pretrained word vectors, sum them. A token with no vector (out
of word_vectors, or padding past the name's real token count)
contributes a zero vector -- not a random placeholder, matching
OpenEA's own np.zeros row for out-of-vocabulary words, so a
name entirely absent from word_vectors just gets a zero-vector
init rather than a fabricated one.
Uses Entity.labels (already populated by this repo's dataset
loaders, see
EnFr15KAttrDataset) as the
"local name" source rather than OpenEA's own
_get_local_name_by_name_triple URI-parsing -- confirmed a no-op
deviation for EN-FR-15K specifically: that function's dataset-specific
name-attribute matching (D_Y/D_W special-cased predicate
lists) never triggers for this dataset (falls to its
else: name_attribute_list = {} branch), so OpenEA's own code
already falls back to the entity URI's tail segment here -- the same
text this repo's loader already populates as the label.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entities
|
list[Entity]
|
Entities needing an initial embedding (both KGs, concatenated by the caller). |
required |
word_vectors
|
dict[str, NDArray[floating[Any]]]
|
Pretrained word vectors, e.g. from load_fasttext_vectors. |
required |
dim
|
int
|
Embedding dimensionality -- must match |
required |
default_length
|
int
|
Max tokens summed per entity. OpenEA's published
value is |
4
|
Returns:
| Type | Description |
|---|---|
dict[str, NDArray[float32]]
|
|
Source code in src/linkingtk/algorithms/ea/_rdgcn_training.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |
_rdgcn_torch ¶
PyTorch model and training-step functions for RDGCNLinker.
Ports OpenEA's Layer/RDGCN (approaches/rdgcn.py): two rounds of
dual-graph (relation) attention projected onto the primal (entity) graph via
sparse attention, each mixed back into the running entity embedding by a
learned scalar (alpha/beta), followed by two diagonal-weight GCN
layers each wrapped in a highway gate. Uses plain torch.sparse
throughout (including torch.sparse.softmax, PyTorch's own built-in
sparse-row-softmax primitive) -- no torch_geometric/dgl dependency, same
posture as _gcn_align_torch.py.
Callers must already have confirmed torch is importable
(rdgcn.py's fit() does this via OptionalDependencyError before
any of these run) -- same precedent as _gcn_align_torch.py/
_kdcoe_torch.py.
Dimension convention (confirmed by reading rdgcn.py's Layer.build()
directly): entity/primal embeddings have dimensionality D (the
constructor's embedding_dim, which must equal the pretrained word
vectors' own dimensionality -- no projection layer exists between them).
Per-relation "dual graph" features are 2 * D (concat([mean head-entity
embeds, mean tail-entity embeds])) -- every dense-attention layer's query
projection takes 2 * D in and produces D out; the sparse attention
layer's relation-score projection takes 2 * D in and produces a single
scalar per edge.
build_rdgcn_model ¶
build_rdgcn_model(
initial_primal_embeddings: Tensor,
embedding_dim: int,
alpha: float,
beta: float,
) -> torch.nn.Module
Ports Layer.build()'s computation graph as a live PyTorch model.
A factory function rather than a module-level torch.nn.Module
subclass, since torch.nn.Module can't be named at module scope
without torch installed -- same precedent as
_gcn_align_torch.build_gcn_align_branch/
_kdcoe_torch.build_description_encoder.
Unlike
build_gcn_align_branch,
the returned model's forward() takes the fixed graph structures
(adjacencies/masks/edge lists) as arguments rather than storing them
internally -- they never change during training (RDGCN has no
AliNet-style bootstrapping adjacency mutation), so this just keeps
device-placement simple: build every fixed tensor directly on the
target device before constructing the model, same as
gcn_align.py's own fit() does for its adjacency.
Confirmed from Layer.build() directly: OpenEA's own
get_pretrained_input wraps the name-embedding init in a plain
tf.Variable (trainable by default, no trainable=False) --
primal_x0 is a trained parameter seeded from the pretrained
name embeddings, not a frozen input. This is why the per-relation
"dual graph" features (which depend on the live primal embeddings)
must be recomputed every forward pass, not cached once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_primal_embeddings
|
Tensor
|
|
required |
embedding_dim
|
int
|
Entity-embedding dimensionality ( |
required |
alpha
|
float
|
Round-1 mixing weight ( |
required |
beta
|
float
|
Round-2 mixing weight. OpenEA's published value is |
required |
Returns:
| Type | Description |
|---|---|
Module
|
A module whose ``forward(primal_adjacency, dual_adjacency, |
Module
|
head_mask, tail_mask, edge_heads, edge_tails, edge_relations)`` |
Module
|
returns |
Module
|
see each argument's shape in |
Module
|
|
Module
|
call site. |
Source code in src/linkingtk/algorithms/ea/_rdgcn_torch.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | |
mine_hard_negatives ¶
mine_hard_negatives(
embeddings: Tensor,
pos_left: Tensor,
pos_right: Tensor,
k: int,
) -> tuple[
torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
]
Manhattan-distance hard-negative mining, per OpenEA's get_neg.
For each seed pair's left/right anchor entity, the k nearest
(L1/cityblock distance) other entities in the current embedding
space become that side's negatives -- unlike GCN-Align's uniform
random sampling. Uses torch.cdist (GPU-friendly) rather than
scipy.spatial.distance.cdist -- with a real seed-pair count in the
thousands and an entity count in the tens of thousands, computing this
on CPU every refresh would be a genuine bottleneck (same "vectorize/
accelerate before trusting a smaller-scale precedent" lesson as
_bootea_training.encode_triples's profiling note).
Deliberate deviation: OpenEA's own get_neg never excludes the
anchor entity itself from its own candidate pool -- since the anchor's
distance to itself is always the global minimum (0), it's very
often ranked as its own "hardest negative," which is meaningless as a
training signal (the corresponding loss term becomes relu(margin +
pos_dist), effectively a constant that can never be reduced by
training). This function excludes each anchor's own id from its
ranking before taking the top k -- a deliberate fix, not
reproduced as OpenEA's own apparent oversight would have it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
Tensor
|
|
required |
pos_left
|
Tensor
|
|
required |
pos_right
|
Tensor
|
|
required |
k
|
int
|
Negatives mined per pair per side. |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
|
Tensor
|
-- pass directly to |
Tensor
|
|
Tensor
|
|
tuple[Tensor, Tensor, Tensor, Tensor]
|
|
tuple[Tensor, Tensor, Tensor, Tensor]
|
neighbor ids. |
Source code in src/linkingtk/algorithms/ea/_rdgcn_torch.py
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
AliNet¶
alinet ¶
AliNet knowledge-graph-embedding linker for Entity Alignment.
Sun, Z., Wang, C., Hu, W., Chen, M., Dai, J., Zhang, W., & Qu, Y. (2020). Knowledge Graph Alignment Network with Gated Multi-hop Neighborhood Aggregation. AAAI 2020. https://ojs.aaai.org/index.php/AAAI/article/view/5354
The third GNN-based EA linker in this package (see #18's parent issue,
42's GCNAlignLinker,¶
43's RDGCNLinker). AliNet¶
combines a 1-hop GraphConvolution
branch with a 2-hop gated-attention branch at every layer but the last,
fusing them with a highway gate, and represents each entity by
concatenating every layer's (plus the initial embedding's) output --
letting non-isomorphic local neighborhoods between the two KGs still align
through a longer-range, multi-hop signal. This is a faithful port of
OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA/blob/master/src/openea/approaches/alinet.py),
not a from-scratch reading of the paper -- see
_alinet_training and
_alinet_torch for the algorithm's
own docstrings, and this module's deviations below (confirmed against
OpenEA's own reference config, run/args/alinet_args_15K.json, and
source):
- Bootstrapping is implemented but off by default, matching what
OpenEA's own published config actually runs -- not the milestone
planning assumption that it's "core to published performance."
Reading
AliNet.run()directly: bootstrapping (augment_neighborhood()) is only calledif self.args.sim_th > 0.0, and the real published config's own value is"sim_th": 0.0--0.0 > 0.0isFalse, so OpenEA's own published EN-FR-15K-V1 numbers never invoke bootstrapping at all. This port replicates that exactly:sim_thdefaults to0.0(bootstrapping off); passsim_th > 0.0to opt in. - No published EN-FR-15K-V1 number exists for AliNet to benchmark
against in
docs/detailed_results_current_approaches_15K.csv-- added to OpenEA after that CSV's last update; its numbers live only in an external Google Sheet linked from OpenEA's README, not reliably fetchable.examples/alinet_benchmark.pyexists for manual sanity-checking only, with no target to compare against. - The relation-consistency auxiliary loss (
compute_rel_loss,rel_param) is not implemented. OpenEA's own published config uses a small non-zerorel_param: 0.01(i.e. it is nominally active, correcting an earlier assumption that it's off by default) -- skipped here since no numeric target exists to validate it against anyway, and it needs its ownrel_win_size-batched relation-window sampling machinery for what's a secondary loss term. - Uniform negative sampling only. OpenEA's own published config uses
neg_sampling: "truncated"(nearest-neighbor-restricted negatives after the first validation checkpoint) -- not implemented; see sample_uniform_cross_kg_negatives's docstring. - Dropout and L2 weight regularization are not implemented -- both
faithful no-ops (OpenEA's own config uses
dropout: 0.0, and its training graph never sums the declared weight regularizers into the actual loss). See _alinet_torch's module docstring. - Candidate scoring uses cosine similarity, unlike
GCNAlignLinker/
RDGCNLinker (both switched
to Manhattan distance -- see their own module docstrings for why).
Checked, not overlooked: OpenEA's own config for this method is
eval_metric: "inner"(raw dot product) over embeddings its own_eval_test_embeddings/saveL2-normalize immediately before scoring -- and AliNetModel's ownforward()already returns L2-normalized output (the finalfunctional.normalize(concatenated, dim=1)call), so raw inner product over these embeddings is mathematically identical to cosine similarity (a . b == cosine(a, b)when|a| = |b| = 1). No change needed here.
AliNetLinker ¶
AliNetLinker(
layer_dims: list[int] | None = None,
num_epochs: int = 500,
learning_rate: float = 0.001,
batch_size: int = 3000,
neg_triple_num: int = 10,
neg_margin: float = 1.5,
neg_margin_balance: float = 0.1,
sim_th: float = 0.0,
bootstrap_start_epoch: int = 20,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Scores candidate pairs via AliNet's gated multi-hop GCN/attention embeddings.
Must be fit before link can be called. See the module docstring for what's ported vs. deliberately deviated from OpenEA.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_dims
|
list[int] | None
|
Layer width sequence -- |
None
|
num_epochs
|
int
|
Training epochs. OpenEA's config allows up to |
500
|
learning_rate
|
float
|
Adam's learning rate. OpenEA's published value is
|
0.001
|
batch_size
|
int
|
Positive pairs sampled per training step (with
replacement, matching OpenEA). OpenEA's published value is
|
3000
|
neg_triple_num
|
int
|
Negatives drawn per positive pair ( |
10
|
neg_margin
|
float
|
Margin for
margin_ranking_loss_l2_squared.
OpenEA's published value is |
1.5
|
neg_margin_balance
|
float
|
Weight applied to the negative-pair loss term.
OpenEA's published value is |
0.1
|
sim_th
|
float
|
Minimum similarity for a bootstrapped pair to be accepted
-- also the bootstrapping on/off switch: OpenEA's own
|
0.0
|
bootstrap_start_epoch
|
int
|
First epoch a bootstrapping round may run,
if |
20
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/alinet.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
random_state: int | None = None,
val_ground_truth: list[tuple[str, str]] | None = None,
patience: int = 5,
eval_every: int = 10,
) -> AliNetLinker
Train the gated multi-hop GCN/attention stack, optionally bootstrapping new pairs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities -- also used to partition |
required |
dataset2
|
list[Entity]
|
Target entities. See |
required |
ground_truth
|
list[tuple[str, str]]
|
List of |
required |
graph
|
Graph
|
The combined relational structure of both KGs (e.g.
|
required |
random_state
|
int | None
|
Seed for reproducible training. Left unspecified, training is non-deterministic. |
None
|
val_ground_truth
|
list[tuple[str, str]] | None
|
Optional held-out pairs used for early
stopping -- every |
None
|
patience
|
int
|
Number of non-improving |
5
|
eval_every
|
int
|
How often (in epochs) to check |
10
|
Returns:
| Type | Description |
|---|---|
AliNetLinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If none of |
OptionalDependencyError
|
If torch isn't installed. |
Source code in src/linkingtk/algorithms/ea/alinet.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/alinet.py
394 395 396 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/alinet.py
398 399 400 | |
_alinet_training ¶
Private, plain-numpy (torch-free) helpers for AliNetLinker.
Ported from OpenEA's reference implementation
(https://github.com/nju-websoft/OpenEA -- approaches/alinet.py's
generate_2hop_triples/enhance_triples/no_weighted_adj, and
AliNet.augment/augment_neighborhood's bootstrapping loop).
Independently testable without torch installed -- see
_alinet_torch.py for the GCN/attention/highway model and training-step
functions.
Reuses two functions already ported for sibling linkers rather than
reimplementing them:
build_primal_adjacency
(RDGCN's unweighted-symmetric-adjacency builder is exactly OpenEA's own
no_weighted_adj's edge-set construction -- used here for the 1-hop
adjacency, given a triple list; the 2-hop adjacency's own
pairs_to_symmetric_adjacency
below is the same construction over a raw pair set instead) and
find_new_pairs
(IPTransE's row-argmax-plus-threshold match finder is exactly OpenEA's own
find_alignment(sim_mat, sim_th, k=1), which is what AliNet's own
augment calls too).
enhance_triples ¶
enhance_triples(
triples1: NDArray[int64],
triples2: NDArray[int64],
seed_pairs: list[tuple[int, int]],
) -> tuple[
set[tuple[int, int, int]], set[tuple[int, int, int]]
]
Cross-KG-inferred triples, per OpenEA's enhance_triples.
For each KG1 triple (h1, r, t1) whose both entities have a seed
counterpart (h2 = links[h1], t2 = links[t1]), infers a matching
KG2 triple (h2, r, t2) -- unless KG2 already has a direct h2 ->
t2 edge (avoiding redundant edges). Mirrored for KG2 -> KG1. This is
what lets a single seed pair "bridge" one KG's own relational
structure into the other's, making the eventual 1-hop adjacency
cross-KG-connected rather than two disjoint components.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples1
|
NDArray[int64]
|
KG1's own |
required |
triples2
|
NDArray[int64]
|
KG2's own triples, same id space. |
required |
seed_pairs
|
list[tuple[int, int]]
|
|
required |
Returns:
| Type | Description |
|---|---|
set[tuple[int, int, int]]
|
|
set[tuple[int, int, int]]
|
add to KG1's/KG2's own triple sets before adjacency construction. |
Source code in src/linkingtk/algorithms/ea/_alinet_training.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
generate_2hop_pairs ¶
generate_2hop_pairs(
triples: NDArray[int64],
skip_top_k: int = _SKIP_TOP_K_PATTERNS,
) -> set[tuple[int, int]]
Entity pairs reachable by a 2-hop path, filtered by relation-pattern frequency.
Ports OpenEA's generate_2hop_triples: finds every (h, m, t)
path (h -r1-> m -r2-> t) whose (h, t) pair is not already a
direct 1-hop edge (either direction), groups them by (r1, r2)
relation-composition pattern, and keeps only paths whose pattern is
not among the skip_top_k most frequent -- confirmed from
reading generate_2hop_triples directly: it computes (but never
uses) a p=0.05-proportional cutoff (num = int(p *
len(relation_patterns))) and its commented-out original loop
(# for i in range(20, num)), but the actual active loop is
for i in range(5, len(relation_patterns)) -- i.e. only the top-5
most frequent patterns are ever excluded, num/p are dead code.
Ported as what's actually executed, not the apparent original intent.
The relation ids and the synthetic self-loop/composed-relation triples
OpenEA's own version builds around this selection are dropped here --
downstream
(pairs_to_symmetric_adjacency)
only needs (h, t) connectivity, not relation ids, and the
synthetic self-loop triple (head, 0, head) OpenEA's own code adds is
already redundant with
normalize_adjacency_coo's
own add_self_loops.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
NDArray[int64]
|
One KG's own |
required |
skip_top_k
|
int
|
Number of most-frequent |
_SKIP_TOP_K_PATTERNS
|
Returns:
| Type | Description |
|---|---|
set[tuple[int, int]]
|
|
Source code in src/linkingtk/algorithms/ea/_alinet_training.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
pairs_to_symmetric_adjacency ¶
pairs_to_symmetric_adjacency(
pairs: set[tuple[int, int]], num_entities: int
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]
Unweighted symmetric adjacency from a raw {(head, tail)} pair set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pairs
|
set[tuple[int, int]]
|
Entity-id pairs, e.g. from generate_2hop_pairs. |
required |
num_entities
|
int
|
Total entity count (both KGs combined). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[float64]
|
both directions present, ready for |
tuple[NDArray[int64], NDArray[float64]]
|
Source code in src/linkingtk/algorithms/ea/_alinet_training.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
run_bootstrapping_round ¶
run_bootstrapping_round(
embeddings: NDArray[floating[Any]],
seed_pairs: list[tuple[int, int]],
unaligned1: list[int],
unaligned2: list[int],
sim_th: float = 0.0,
) -> tuple[list[tuple[int, int]], list[int], list[int]]
One bootstrapping round: find new high-confidence pairs, expand the seed set.
Ports OpenEA's augment/augment_neighborhood: cosine similarity
between every still-unaligned KG1 entity and every still-unaligned KG2
entity, find_new_pairs
(= OpenEA's own find_alignment(sim_mat, sim_th, k=1), row-wise
top-1 plus threshold) for one-sided candidate matches, then a
column-collision resolution step (keep only the highest-similarity
match per KG2 target) standing in for OpenEA's separate, stateful
update_labeled_alignment_x/_y editing passes -- once a pair is
accepted here it's added to the seed set permanently, rather than
remaining revisable in later rounds the way OpenEA's own labeled-
alignment dict does. A simpler, one-directional approximation of the
same "avoid many-to-one collisions" property, not a full port.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
NDArray[floating[Any]]
|
|
required |
seed_pairs
|
list[tuple[int, int]]
|
Current seed set (both original and previously bootstrapped pairs). |
required |
unaligned1
|
list[int]
|
KG1 entity ids not yet in |
required |
unaligned2
|
list[int]
|
KG2 entity ids not yet in |
required |
sim_th
|
float
|
Minimum cosine similarity for a match to count. OpenEA's
published value is |
0.0
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
|
list[int]
|
|
list[int]
|
round; the two remaining lists have those entities removed. |
Source code in src/linkingtk/algorithms/ea/_alinet_training.py
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | |
sample_uniform_cross_kg_negatives ¶
sample_uniform_cross_kg_negatives(
entities1: list[int],
entities2: list[int],
batch_size: int,
neg_triple_num: int,
rng: Generator,
exclude_pairs: set[tuple[int, int]],
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]
Fully-random cross-KG negative pairs, per OpenEA's generate_input_batch's uniform branch.
Unlike GCN-Align's/RDGCN's negatives (each anchored to a real positive
pair's own entity), AliNet's uniform negatives are independently
drawn from each KG's own entity pool -- neg_ent1[i]/neg_ent2[i]
are unrelated random draws, not a corruption of a specific positive
pair. Pairs that happen to collide with a known positive
(exclude_pairs -- the current seed set, so a "negative" is never
accidentally a real alignment) are dropped.
Deliberate scope reduction: OpenEA's own published config uses
neg_sampling: "truncated", switching to nearest-neighbor-restricted
negatives (via find_neighbors) after the first validation
checkpoint -- not implemented here; every epoch uses this uniform
scheme. See the module docstring on
alinet for why.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entities1
|
list[int]
|
Candidate KG1-side entity ids to draw from (OpenEA's
own pool is |
required |
entities2
|
list[int]
|
Candidate KG2-side entity ids. |
required |
batch_size
|
int
|
Positive pairs in this batch. |
required |
neg_triple_num
|
int
|
Negatives drawn per positive pair ( |
required |
rng
|
Generator
|
Source of randomness. |
required |
exclude_pairs
|
set[tuple[int, int]]
|
Pairs to drop if drawn (the current seed set). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[int64]
|
|
NDArray[int64]
|
(shorter if some draws were excluded). |
Source code in src/linkingtk/algorithms/ea/_alinet_training.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | |
_alinet_torch ¶
PyTorch model and training-step functions for AliNetLinker.
Ports OpenEA's GraphConvolution/AliNetGraphAttentionLayer/
HighwayLayer/AliNet._define_model (approaches/alinet.py): a
stack of layers where every layer but the last combines a 1-hop
GraphConvolution branch and a 2-hop AliNetGraphAttentionLayer
branch, fused by a HighwayLayer gate; the last layer is
GraphConvolution only. The final representation concatenates every
layer's (plus the initial embedding's) L2-normalized output -- a
jumping-knowledge-style multi-scale representation. Uses plain
torch.sparse throughout (including torch.sparse.softmax, same as
_rdgcn_torch.py) -- no torch_geometric/dgl dependency.
Callers must already have confirmed torch is importable
(alinet.py's fit() does this via OptionalDependencyError
before any of these run) -- same precedent as _gcn_align_torch.py/
_rdgcn_torch.py.
Dropout and L2 weight regularization are not implemented. OpenEA's own
published config uses dropout: 0.0 (never actually triggers the
reference's own dropout branches), and while every learnable weight in
the reference is declared with an l2_regularizer(scale=0.01), nothing
in AliNet's own training graph construction (_generate_graph/
_generate_rel_graph) ever retrieves and sums TensorFlow's
regularization-losses collection into the actual optimized loss --
confirmed by reading AliNet's class body directly. Both are faithful
no-ops to skip, not simplifications with real fidelity cost.
This is the only linker in this package whose adjacency mutates during
training -- AliNetModel.set_adjacency is called once before training
and again after every bootstrapping round (see
AliNetLinker.fit),
updating every layer's stored sparse adjacency in place rather than
rebuilding the model.
build_alinet_model ¶
build_alinet_model(
num_entities: int, layer_dims: list[int]
) -> torch.nn.Module
Ports AliNet._define_model's layer stack as a live PyTorch model.
A factory function rather than a module-level torch.nn.Module
subclass, since torch.nn.Module can't be named at module scope
without torch installed -- same precedent as
_gcn_align_torch.build_gcn_align_branch/
_rdgcn_torch.build_rdgcn_model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_entities
|
int
|
Total entity count (both KGs combined). |
required |
layer_dims
|
list[int]
|
Layer width sequence, e.g. |
required |
Returns:
| Type | Description |
|---|---|
Module
|
A module exposing ``set_adjacency(one_hop_adjacency, |
Module
|
two_hop_adjacency)`` (must be called before the first |
Module
|
|
Module
|
are coalesced sparse |
Module
|
e.g. from |
Module
|
|
Module
|
and |
Module
|
sum(layer_dims) + layer_dims[0])`` -- the concatenated, |
Module
|
L2-normalized multi-layer representation, every layer's output |
Module
|
plus the initial embedding, matching OpenEA's own |
Module
|
|
Source code in src/linkingtk/algorithms/ea/_alinet_torch.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | |
Simple-HHEA¶
simple_hhea ¶
Simple-HHEA linker for Entity Alignment.
Ports Simple-HHEA (Jiang et al. 2024, "Toward Practical Entity Alignment Method Design: Insights from New Highly Heterogeneous Knowledge Graph Datasets", https://dl.acm.org/doi/pdf/10.1145/3589334.3645720) from its reference implementation at https://github.com/DataArcTech/Simple-HHEA -- a base embedding method for highly heterogeneous KG pairs (e.g. an event KG aligned to Wikipedia), fusing ALBERT name embeddings (whitened), node2vec structural embeddings, and (optionally) Time2Vec temporal embeddings through a small MLP trained with margin ranking loss.
Filed as #62, a prerequisite for #22 (ChatEA): the ChatEA paper's
published llama2-13b numbers are ChatEA layered on top of Simple-HHEA
specifically, not a generic KGE method, so SimpleHHEALinker is what
ChatEALinker will use as its base_linker for a genuine comparison.
See linkingtk.algorithms.ea._simple_hhea_name/_simple_hhea_structure/
_simple_hhea_torch/_simple_hhea_training for the individual feature-
group ports and their documented fidelity decisions vs. the reference.
SimpleHHEALinker ¶
SimpleHHEALinker(
name_model: str = "albert-base-v2",
emb_size: int = 64,
structure_size: int = 8,
time_size: int = 8,
use_structure: bool = True,
walk_length: int = 80,
num_walks: int = 10,
structure_dim: int = 64,
max_degree: int = 1000,
structure_workers: int = 1,
structure_epochs: int = 5,
num_epochs: int = 1500,
learning_rate: float = 0.01,
weight_decay: float = 0.001,
gamma: float = 1.0,
matching: Matcher = DEFAULT_MATCHER,
device: str = "cpu",
)
Bases: BaseLinker
Fuses whitened name embeddings, node2vec structural embeddings, and (optionally) Time2Vec temporal embeddings for Entity Alignment.
Must be fit
before link. Exposes source_embedding/target_embedding like
KGELinker, so it's a drop-in
for rank_exhaustive -- and,
once #22 (ChatEA) lands, a candidate base_linker for it too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name_model
|
str
|
Hugging Face model id for name embedding, loaded via
|
'albert-base-v2'
|
emb_size
|
int
|
Final fused embedding dimension. |
64
|
structure_size
|
int
|
Post-projection size of the structure branch before concatenation into the final embedding. |
8
|
time_size
|
int
|
Post-projection size of the time branch before concatenation into the final embedding. |
8
|
use_structure
|
bool
|
Whether to compute and use node2vec structural
embeddings at all (the reference's own |
True
|
walk_length
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
80
|
num_walks
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
10
|
structure_dim
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
64
|
max_degree
|
int
|
node2vec walk hyperparameter -- see build_structure_embeddings. |
1000
|
structure_workers
|
int
|
Worker threads for the structural Word2Vec fit -- see build_structure_embeddings. Walk simulation itself is always single-threaded, matching the reference's own default invocation. |
1
|
structure_epochs
|
int
|
Word2Vec training epochs for the structural skip-gram model -- see build_structure_embeddings. |
5
|
num_epochs
|
int
|
Training hyperparameter -- see train. |
1500
|
learning_rate
|
float
|
Training hyperparameter -- see train. |
0.01
|
weight_decay
|
float
|
Training hyperparameter -- see train. |
0.001
|
gamma
|
float
|
Training hyperparameter -- see train. |
1.0
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
device
|
str
|
Torch device to train/embed on, e.g. |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
fit ¶
fit(
dataset1: list[Entity],
dataset2: list[Entity],
ground_truth: list[tuple[str, str]],
graph: Graph,
temporal_triples: list[TemporalTriple] | None = None,
random_state: int | None = None,
) -> SimpleHHEALinker
Trains the fusion model over dataset1/dataset2's entities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset1
|
list[Entity]
|
Source entities. |
required |
dataset2
|
list[Entity]
|
Target entities. |
required |
ground_truth
|
list[tuple[str, str]]
|
Known-correct |
required |
graph
|
Graph
|
Combined relational structure of both KGs (e.g.
|
required |
temporal_triples
|
list[TemporalTriple] | None
|
Optional |
None
|
random_state
|
int | None
|
Seed for reproducible training. |
None
|
Returns:
| Type | Description |
|---|---|
SimpleHHEALinker
|
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If no |
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | |
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
285 286 287 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/simple_hhea.py
289 290 291 | |
_simple_hhea_name ¶
ALBERT name-embedding + whitening for SimpleHHEALinker.
Ports process_name_embedding.py from
https://github.com/DataArcTech/Simple-HHEA: mean-pooled ALBERT embeddings
of each entity's label text, followed by whitening (Su et al. 2021,
"Whitening Sentence Representations for Better Semantics and Faster
Retrieval") -- a linear transform derived from the SVD of the combined
covariance of both KGs' raw embeddings that reduces dimensionality and
improves cosine-similarity behavior of BERT-family sentence embeddings.
transformers/torch are core dependencies already, so this needs no
OptionalDependencyError guard (unlike the structural-embedding stage's
new node2vec dependency).
compute_kernel_bias ¶
compute_kernel_bias(
vectors: NDArray[floating[Any]], n_components: int
) -> tuple[
npt.NDArray[np.floating[Any]],
npt.NDArray[np.floating[Any]],
]
Whitening kernel/bias from vectors' covariance.
Ports process_name_embedding.py's compute_kernel_bias -- the
final transform is (x + bias) @ kernel.
Source code in src/linkingtk/algorithms/ea/_simple_hhea_name.py
79 80 81 82 83 84 85 86 87 88 89 90 91 | |
embed_names ¶
embed_names(
entities: list[Entity],
model_name: str = "albert-base-v2",
device: str = "cpu",
batch_size: int = 64,
) -> npt.NDArray[np.floating[Any]]
Mean-pooled ALBERT embedding for each entity's cleaned label text.
Returns a raw (len(entities), hidden_size) array -- not whitened.
compute_kernel_bias/
whiten must be fit
jointly over both KGs' raw embeddings (matching the reference's own
combined-covariance whitening), so aren't folded in here.
Source code in src/linkingtk/algorithms/ea/_simple_hhea_name.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
entity_name_text ¶
entity_name_text(entity: Entity) -> str
Cleaned label text for name-embedding, per the reference's own
name.split("/")[-1].replace("_", " ") (strips a URI down to its
trailing local name, then de-underscores it).
Source code in src/linkingtk/algorithms/ea/_simple_hhea_name.py
29 30 31 32 33 34 | |
whiten ¶
whiten(
vectors: NDArray[floating[Any]],
kernel: NDArray[floating[Any]],
bias: NDArray[floating[Any]],
) -> npt.NDArray[np.floating[Any]]
Applies a whitening transform and L2-normalizes each row.
Ports process_name_embedding.py's transform_and_normalize.
Source code in src/linkingtk/algorithms/ea/_simple_hhea_name.py
94 95 96 97 98 99 100 101 102 103 104 105 106 | |
_simple_hhea_structure ¶
node2vec structural embeddings for SimpleHHEALinker.
Ports the biased-random-walk structural-embedding stage of
https://github.com/DataArcTech/Simple-HHEA
(feature_perprocessing/preproccess.py + longterm/{main.py,node2vec.py}
+ get_deep_emb.py) directly from its own hand-rolled alias-sampling walk
code in longterm/node2vec.py -- not the third-party node2vec PyPI
package this module first used.
That first attempt was a real, confirmed bug, not just a fidelity gap:
longterm/node2vec.py's get_alias_edge uses a non-standard formula
(weight * p for the "return to the previous node" term -- the file
even has the textbook weight / p commented out directly above it, a
deliberate inversion of the Grover & Leskovec convention). The node2vec
PyPI package implements the standard (divide-by-p) formula, so
copying the reference's literal p=1e-100 into it did the opposite of
the intended "never immediately backtrack" -- confirmed directly: sampled
walks degenerated into pure two-node oscillation (['d','c','d','c',
...]). Retuning p for the standard package's own convention (p=100)
produced genuinely exploratory walks, but scored far worse on the real
ICEWS-WIKI benchmark than the degenerate ones (Hits@1 0.037-0.041 vs.
0.262, across several configurations, including 10x more Word2Vec epochs
and a 10x harder degree cap -- ruling out undertraining and hub-node
dilution as the cause). The reference's actual formula isn't equivalent
to either end of the standard package's p/q dial: at q=1, its elif
branch (a neighbor that's also adjacent to the previous node) gets
zeroed out too, alongside the return term -- a third walk character the
standard package has no way to express. Hand-porting the exact formula
below, rather than continuing to search for an equivalent
parameterization, is the only way to reproduce it.
Ported near-verbatim: _alias_setup/_alias_draw (the classic
alias-method sampling utilities), _precompute_transition_probs/
_get_alias_edge/_node2vec_walk/_simulate_walks (the reference's
Graph.preprocess_transition_probs/get_alias_edge/node2vec_walk/
simulate_walks). Differences from the reference's own code, all
mechanical, none behavioral:
_alias_drawtakes an explicitnp.random.Generatorinstead of reading numpy's global random state (this module already seeds everything else that way).np.int(removed in modern numpy) becomesnp.int64.- Each node's sorted neighbor list is computed once and cached
(
_sorted_neighbors), not recomputed by a freshsorted(G.neighbors(...))call on every single walk step and everyget_alias_edgecall like the reference does -- pure performance (the ordering, and therefore every alias table's meaning, is identical either way; a real-graph hub node still at themax_degreecap would otherwise be re-sorted on every one of its many visits).
Two deliberate simplifications vs. the reference (flagged in #62's plan, not silent):
- Walks are entity-only, with no relation-id interleaving. The reference splices relation ids into each walk (entity-relation-entity-...) before handing it to Word2Vec, but the paper text itself never mentions this and it isn't ablated separately -- treated here as a reference-code-only embellishment on top of "a biased random walk method", not the core contribution.
- The "merge known-aligned entities into one graph node before walking"
trick (the reference's
node2same) is driven directly by thetrain_pairsSimpleHHEALinker.fit already receives asground_truth, instead of reverse-engineering the reference'sref_ent_ids[:1500]file-position slice. Each pair's target id is remapped onto its source id directly (one fewer moving part than the reference's brand-new shared id per pair) -- functionally the same "one shared node" outcome.
One addition the reference doesn't have at all, found necessary against
the real ICEWS-WIKI dataset (not a toy/synthetic graph): its combined
graph has an extremely power-law-skewed degree distribution (median node
degree 18, but the single highest-degree node -- "United States", ICEWS
event data's most common actor -- has 273,317 edges; confirmed by direct
measurement, not assumed). The alias-table transition-probability
precompute is O(degree) per edge, so a hub node alone costs
O(degree^2) -- tens of billions of operations for that one node, which
never finished in practice with the original (uncapped) graph (confirmed:
killed after several CPU-bound minutes stuck in exactly that precompute
step, well before any walk was even simulated).
cap_node_degree
randomly subsamples any node's incident edges down to max_degree before
walking -- a standard, well-precedented mitigation for random-walk
embeddings on scale-free graphs (not something the reference needed to
consider, or that a toy-scale unit-test fixture would ever surface).
build_structure_embeddings ¶
build_structure_embeddings(
entity_ids: list[str],
triples: list[Triple],
train_pairs: list[tuple[str, str]],
*,
dimensions: int = 64,
walk_length: int = 80,
num_walks: int = 10,
p: float = 1e-100,
q: float = 1.0,
window: int = 10,
epochs: int = 5,
workers: int = 1,
max_degree: int = 1000,
random_state: int | None = None,
) -> dict[str, npt.NDArray[np.floating[Any]]]
One structural embedding per entity_ids, via a biased random walk + skip-gram.
p/q/walk_length/num_walks/window/epochs default
to the reference's own values -- see this module's docstring for why
p=1e-100 (a tiny, near-zero value) is correct here, ported from
the reference's own non-standard alias-edge formula (weight * p,
not weight / p), unlike the standard node2vec package's
convention this module used at first. Entities with no edges at all
in triples fall back to a random vector -- ports get_deep_emb.py's
own except: np.random.random_sample(...) fallback for the same
case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
workers
|
int
|
Worker threads for gensim's |
1
|
max_degree
|
int
|
Caps every node's neighbor count before walking (see cap_node_degree) -- needed for real event-KG-scale graphs (e.g. ICEWS-WIKI), whose degree distribution is extreme enough that the transition-probability precompute is otherwise intractable. Irrelevant at toy/unit-test scale. |
1000
|
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/algorithms/ea/_simple_hhea_structure.py
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | |
cap_node_degree ¶
cap_node_degree(
edges: list[tuple[str, str]],
max_degree: int,
rng: Generator,
) -> list[tuple[str, str]]
Randomly subsamples edges so no node ends up with more than max_degree.
Shuffles edges (so which ones survive isn't biased toward triple
order/relation type), then greedily keeps each edge only while both
endpoints are still under max_degree -- a node already at the cap
loses only its "excess" edges, not a uniform fraction of all of them,
so a rare hub node is trimmed while everything else is left alone.
Source code in src/linkingtk/algorithms/ea/_simple_hhea_structure.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
_simple_hhea_torch ¶
Time2Vec + fusion model for SimpleHHEALinker.
Ports model.py from https://github.com/DataArcTech/Simple-HHEA: a small
MLP that concatenates (name, time, structure) feature groups -- each
already computed elsewhere (_simple_hhea_name.py,
_simple_hhea_structure.py, build_time_histogram below) -- into one
final per-entity embedding. Name/time/structure embeddings are held as
fixed buffers (not trained); only the small linear/Time2Vec layers on
top are trainable, matching the reference's plain (non-nn.Parameter)
tensor attributes.
TIME_SPAN
module-attribute
¶
TIME_SPAN = 1 + 27 * 13
Number of month-bins: 1 catch-all pre-1995 bin, plus 27 years * 13
(the reference's own off-by-one month indexing, see _time_bin) monthly
bins -- matches the reference's own 1+27*13, wide enough for ICEWS data
(1995-2021ish).
CosineActivation ¶
CosineActivation(in_features: int, out_features: int)
Bases: Module
Time2Vec's periodic + linear feature bank (Kazemi et al. 2019).
Source code in src/linkingtk/algorithms/ea/_simple_hhea_torch.py
91 92 93 94 95 96 | |
SimpleHHEAModel ¶
SimpleHHEAModel(
name_emb: NDArray[floating[Any]],
time_emb: NDArray[floating[Any]] | None,
structure_emb: NDArray[floating[Any]] | None,
emb_size: int = 64,
structure_size: int = 8,
time_size: int = 8,
device: device | str = "cpu",
)
Bases: Module
Fuses name/time/structure feature groups into one final embedding per entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name_emb
|
NDArray[floating[Any]]
|
|
required |
time_emb
|
NDArray[floating[Any]] | None
|
|
required |
structure_emb
|
NDArray[floating[Any]] | None
|
|
required |
emb_size
|
int
|
Final output embedding dimension. |
64
|
structure_size
|
int
|
Post-projection size of the structure branch before concatenation. |
8
|
time_size
|
int
|
Post-projection size of the time branch before concatenation. |
8
|
device
|
device | str
|
Where to place the fixed feature tensors -- plain
attributes (matching the reference), not registered buffers,
so they don't move with a later |
'cpu'
|
Source code in src/linkingtk/algorithms/ea/_simple_hhea_torch.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
build_time_histogram ¶
build_time_histogram(
entity_ids: list[str],
temporal_triples: list[
tuple[str, str, str, str | None, str | None]
],
) -> npt.NDArray[np.floating[Any]]
Per-entity month-activity histogram, (len(entity_ids), TIME_SPAN).
For each (subject, relation, object, start_label, end_label)
temporal triple, increments the subject's histogram at every month
bin in [start, end] (inclusive) if both resolve; a single bin if
only one resolves; skipped if neither does. A resolved year before
1995 (ICEWS's real lower bound) falls into bin 0, the catch-all
bin. One symmetric rule for every triple -- unlike the reference's own
load_ent_time_matrix, whose two KG-side branches diverge (one
increments two independent point-bins, the other fills a range) and
whose range branch reads a stale time_y left over from an unrelated
earlier loop (a genuine bug, not a deliberate per-side design choice
-- confirmed by reading utils.py directly). Not ported here.
Source code in src/linkingtk/algorithms/ea/_simple_hhea_torch.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
_simple_hhea_training ¶
Margin-ranking training loop for SimpleHHEALinker.
Ports main_SimpleHHEA.py's train()/get_train_set()/l1() from
https://github.com/DataArcTech/Simple-HHEA. Negatives are uniform-random
entity ids, not hard-negative mining -- a simplification already present
in the reference itself, not something to "improve" here.
build_training_quads ¶
build_training_quads(
train_pairs: NDArray[int64],
batch_size: int,
node_size: int,
rng: Generator,
) -> npt.NDArray[np.int64]
Builds a fixed (batch_size, 4) array of (l, r, random_l, random_r) rows.
Ports get_train_set exactly, including its one real quirk: l/r
are train_pairs repeated (not resampled) enough times to reach
batch_size rows (ceil(batch_size / len(train_pairs)) copies,
shuffled, then truncated to exactly batch_size) -- built once,
not per epoch. The reference's own call site passes the node count
as batch_size (not e.g. len(train_pairs)), so train_pairs gets
repeated roughly node_size / len(train_pairs) times; the two random
-negative columns are drawn once here too, then held fixed for the
entire training run (train()'s epoch loop never redraws them).
Source code in src/linkingtk/algorithms/ea/_simple_hhea_training.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
train ¶
train(
model: SimpleHHEAModel,
train_pairs: NDArray[int64],
node_size: int,
*,
num_epochs: int = 1500,
learning_rate: float = 0.01,
weight_decay: float = 0.001,
gamma: float = 1.0,
random_state: int | None = None,
) -> None
Trains model in place via margin ranking loss.
One fixed set of training quads is built up front (see
build_training_quads,
batch_size=node_size matching the reference's own call). Every
epoch: one full forward pass over the entire entity set (model() --
the reference's Simple_HHEA.forward() has no batching, every
entity's fused embedding is recomputed every step), then
sum(relu(gamma + d(l,r) - d(l,r')) + relu(gamma + d(l,r) - d(l',r)))
/ len(quads) where d is L1 distance -- ports main_SimpleHHEA.py's
train() exactly (minus its retain_graph=True, unneeded here since
each epoch builds a fresh graph from a fresh model() call).
Source code in src/linkingtk/algorithms/ea/_simple_hhea_training.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
ChatEA¶
chatea ¶
ChatEA linker for Entity Alignment.
Ports ChatEA (Jiang et al. 2024, "Unlocking the Power of Large Language
Models for Entity Alignment", https://aclanthology.org/2024.acl-long.408.pdf)
from its reference implementation at
https://github.com/DataArcTech/ChatEA -- an LLM re-ranker layered on top
of an embedding-based EA method's own top-K candidates, rather than a
standalone EA method. Filed as #22, a follow-up to #62
(SimpleHHEALinker),
which is the reference's own base embedding method and this linker's
intended base_linker.
See _chatea_context, _chatea_prompts, and _chatea_reasoning for the individual ported pieces and their documented fidelity decisions vs. the reference.
ChatEALinker ¶
ChatEALinker(
base_linker: EmbeddingLinker,
client: LlmClient,
top_k: int = 20,
neigh_num: int = 5,
desc_neigh_num: int = 25,
desc_max_tokens: int = 80,
threshold: float = 0.5,
history_len: int = 3,
relation_names: dict[str, str] | None = None,
temporal_triples: list[TemporalTriple] | None = None,
use_name: bool = True,
use_desc: bool = True,
use_struct: bool = True,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Re-ranks an already-fitted embedding linker's top-K candidates with an LLM.
Unlike most linkers in linkingtk.algorithms.ea, has no fit() --
base_linker must already be fitted before construction (its
embeddings are what generates candidates), and the LLM itself needs
no training. This mirrors
LlmBaseLinker, which is
also fit()-free for the same reason.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_linker
|
EmbeddingLinker
|
An already-fitted linker exposing |
required |
client
|
LlmClient
|
An LlmClient (e.g. from
create_client). This
class never imports |
required |
top_k
|
int
|
Candidates considered per source entity, ranked by
|
20
|
neigh_num
|
int
|
Neighbor triples shown per entity in the reasoning
prompt (the reference's own |
5
|
desc_neigh_num
|
int
|
Neighbor triples shown per entity when
generating its one-sentence description (the reference's
own |
25
|
desc_max_tokens
|
int
|
Forwarded to the description-generation LLM call. |
80
|
threshold
|
float
|
If the base method's own top-2 candidate scores differ by more than this, accept the top-1 candidate immediately with no LLM call at all (the reference's own confidence shortcut). |
0.5
|
history_len
|
int
|
Chat turns (user+assistant pairs) kept in the running conversation history within one source entity's re-ranking. |
3
|
relation_names
|
dict[str, str] | None
|
|
None
|
temporal_triples
|
list[TemporalTriple] | None
|
Optional |
None
|
use_name
|
bool
|
Whether to score name similarity. |
True
|
use_desc
|
bool
|
Whether to generate and score entity descriptions
(requires |
True
|
use_struct
|
bool
|
Whether to score structural (neighbor-tuple) similarity. |
True
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final
links, same as every other linker's |
DEFAULT_MATCHER
|
Note
blocking is accepted for interface compliance but not used --
candidates come from base_linker's own embedding similarity,
not a BlockingStrategy, same pattern
LlmBaseLinker/
StringSimilarityLinker
already document for their own unused params. dataset2 must be
a list[Entity], not an EntitySource -- candidate generation
needs every target's embedding upfront.
Source code in src/linkingtk/algorithms/ea/chatea.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
EmbeddingLinker ¶
Bases: Protocol
What ChatEALinker needs from its base_linker -- the same shape
linkingtk.eval.ranking's own private _ScoringLinker Protocol
uses, duplicated locally since that one is module-private. Every
linker in linkingtk.algorithms.ea (e.g.
SimpleHHEALinker)
already implements this.
source_embedding ¶
source_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's source side.
Source code in src/linkingtk/algorithms/ea/chatea.py
59 60 | |
target_embedding ¶
target_embedding(
entity_id: str,
) -> npt.NDArray[np.floating[Any]]
Vector used to score entity_id as a scored pair's target side.
Source code in src/linkingtk/algorithms/ea/chatea.py
62 63 | |
_chatea_context ¶
Per-entity context (neighbor triples, names, LLM-generated descriptions) for ChatEALinker's prompts.
Ports the neighbor-rendering half of the reference's
tools_for_ChatEA.py's NeighborGenerator.get_neighbors (as
NeighborIndex)
and the description pre-compute of preobtain_description.py's
generate_prompt/get_description (as
generate_descriptions).
NeighborTriple
module-attribute
¶
NeighborTriple = tuple[
str, str, str, "str | None", "str | None"
]
(subject_name, relation_name, object_name, start_label, end_label)
-- time labels are None for non-temporal datasets/triples.
EntityContext
dataclass
¶
EntityContext(
entity_id: str,
name: str,
description: str,
neighbors: list[NeighborTriple] = list(),
)
One entity's rendered prompt content.
NeighborIndex ¶
NeighborIndex(
triples: list[Triple],
temporal_triples: list[TemporalTriple] | None,
entity_names: dict[str, str],
relation_names: dict[str, str],
neigh_num: int,
)
Looks up an entity's name and (capped) rendered neighbor triples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
triples
|
list[Triple]
|
Combined relational triples, used only when
|
required |
temporal_triples
|
list[TemporalTriple] | None
|
Combined temporal triples (preferred over
|
required |
entity_names
|
dict[str, str]
|
|
required |
relation_names
|
dict[str, str]
|
|
required |
neigh_num
|
int
|
Max neighbor triples kept per entity (both as subject
and object), mirroring the reference's own |
required |
Source code in src/linkingtk/algorithms/ea/_chatea_context.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
format_triple ¶
format_triple(triple: NeighborTriple) -> str
Renders one neighbor triple as prompt text, e.g. "(A, rel, B, 2005-11, 2005-11)".
Source code in src/linkingtk/algorithms/ea/_chatea_context.py
100 101 102 103 104 105 | |
generate_descriptions ¶
generate_descriptions(
client: LlmClient,
entity_ids: Iterable[str],
index: NeighborIndex,
*,
max_tokens: int = 80,
) -> dict[str, str]
One cached LLM-generated one-sentence description per id in entity_ids.
Ports preobtain_description.py's get_description -- called once
per entity actually needed (a source entity or one of its top-k
candidates), not once per comparison.
Source code in src/linkingtk/algorithms/ea/_chatea_context.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
_chatea_prompts ¶
Prompt/scoring-schema construction for ChatEALinker.
Ports main_ChatEA.py's init_fields/init_prompt (the system prompt --
task framing plus a worked reasoning example) and generate_prompt/
ask_for_accuracy (the per-candidate reasoning prompt and the
"rethinking" confirmation prompt) -- adapted to score via
LlmClient.complete_structured
against a fixed JSON schema instead of the reference's free-text +
regex-parsed response (get_score), matching
LlmBaseLinker's own existing
convention in this repo.
The reference's use_code/class-definition text-assembly branching
(init_prompt's combinatorial "how do I describe self.tuples in
pseudo-code depending on which flags are off" logic) is simplified here
to one plain description of whichever info types are enabled -- prompt-
text cosmetics for ablations this port isn't running. The actual
algorithm (candidate windowing, weighted scoring, thresholds,
rethinking) lives in
_chatea_reasoning and is
ported faithfully.
ScoringDimensions
dataclass
¶
ScoringDimensions(
keys: tuple[str, ...], weights: dict[str, float]
)
Which of the four score dimensions are active, and their (renormalized) weights.
build_candidate_prompt ¶
build_candidate_prompt(
main: EntityContext,
candidate: EntityContext,
candidate_names: list[str],
ranked_so_far: list[tuple[str, float]],
dims: ScoringDimensions,
) -> tuple[LlmMessage, str]
Ports generate_prompt. Returns (full_message, simple_prompt_text) --
simple_prompt_text is the compact form used for the running chat history
(mirrors the reference's own prompt, simple_prompt pair).
Source code in src/linkingtk/algorithms/ea/_chatea_prompts.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
build_rethink_prompt ¶
build_rethink_prompt(
main_name: str,
candidate_names: list[str],
ranked: list[tuple[str, float, int]],
) -> LlmMessage
Ports ask_for_accuracy's prompt: "are these alignments good enough?".
Source code in src/linkingtk/algorithms/ea/_chatea_prompts.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
build_system_prompt ¶
build_system_prompt(dims: ScoringDimensions) -> str
Ports init_prompt: task framing plus one worked reasoning example.
Source code in src/linkingtk/algorithms/ea/_chatea_prompts.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
resolve_dimensions ¶
resolve_dimensions(
use_name: bool,
use_desc: bool,
use_struct: bool,
use_time: bool,
) -> ScoringDimensions
Picks active dimensions and renormalizes _BASE_WEIGHTS over them.
Mirrors init_fields's scale_weights = [w/sum(weights) for w in weights].
The description dimension requires use_name too, same gating as the
reference (if use_name and use_desc and use_code) -- "does this
description point at the same entity as that name" presupposes a name
was shown at all.
Source code in src/linkingtk/algorithms/ea/_chatea_prompts.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
summarize_response ¶
summarize_response(
response: dict[str, Any], dims: ScoringDimensions
) -> str
A short text summary of a scoring response, used as the assistant's
turn in the running chat history (mirrors the reference's own
simple_response, keeping history compact across many calls).
Source code in src/linkingtk/algorithms/ea/_chatea_prompts.py
98 99 100 101 102 103 | |
weighted_score ¶
weighted_score(
response: dict[str, Any], dims: ScoringDimensions
) -> float
Combines a scoring response into one score in [0, 1].
Ports eval_alignment_for_evaluate's score += weights[j] * (sims[j] - 1) * 0.25.
Source code in src/linkingtk/algorithms/ea/_chatea_prompts.py
85 86 87 88 89 90 91 92 93 94 95 | |
_chatea_reasoning ¶
Per-source-entity iterative LLM re-ranking loop for ChatEALinker.
Ports main_ChatEA.py's eval_alignment_for_evaluate -- the confidence
shortcut, widening candidate windows, weighted scoring, and the
stop/continue/rethink control flow -- minus its ref_ent/base_rank
ground-truth-peeking shortcut (the reference skips its LLM loop entirely
when the true target isn't even in the top-20 candidates, since no
re-ranking could ever reach it; that's pure benchmark-cost accounting
using ground truth only evaluation code has access to).
BaseLinker.link's general
interface has no ground truth to peek at, so keeping that shortcut here
would be a real interface violation, not a faithful port -- its
cost-saving effect is instead reproduced at the benchmark-script layer,
where ground truth is legitimately available (see
examples/chatea_ea.py).
Structured JSON scoring (via _chatea_prompts) replaces the reference's regex-parsed free text, matching LlmBaseLinker's own convention.
rerank_candidates ¶
rerank_candidates(
client: LlmClient,
system_prompt: str,
main: EntityContext,
candidates: list[tuple[EntityContext, float]],
config: ReasoningConfig,
) -> dict[str, float]
Re-ranks candidates (base-method context + score, sorted descending
by base score) for main, returning entity_id -> final weighted score
for every candidate actually reasoned about by the LLM.
Ports eval_alignment_for_evaluate. An empty candidates returns {}.
Source code in src/linkingtk/algorithms/ea/_chatea_reasoning.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
Entity Linking¶
el ¶
Entity Linking (EL) algorithms.
EL links named-entity mentions in text (with context, no description) to entries in a knowledge base (with description, no context). Related systems: spaCy's EntityLinker, ReFinED, BLINK.
BlinkEncoder ¶
BlinkEncoder(
mention_model_name: str = "distilbert-base-uncased",
entity_model_name: str | None = None,
embedding_dim: int = 256,
max_length: int = 96,
encode_batch_size: int = 64,
)
Bases: Module
Two independently-parameterized transformer bi-encoder for Entity Linking.
Satisfies TrainableModel:
encode() dispatches each entity to a mention-side or entity-side
sub-transformer (by whether entity.context is not None, mirroring
ReFinEDEncoder's
dispatch) -- unlike ReFinEDEncoder, the two sides share no weights
at all, BLINK's real distinguishing idea.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mention_model_name
|
str
|
Hugging Face model id for the mention-side
encoder, loaded via |
'distilbert-base-uncased'
|
entity_model_name
|
str | None
|
Hugging Face model id for the entity-side
encoder. Defaults to the same as |
None
|
embedding_dim
|
int
|
Output embedding dimension for both sides, independent of either backbone's native hidden size. |
256
|
max_length
|
int
|
Maximum token length per encoded text (truncated). |
96
|
encode_batch_size
|
int
|
Maximum number of texts forwarded through a
sub-transformer in one go; |
64
|
Source code in src/linkingtk/algorithms/el/blink.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
encode ¶
encode(entities: list[Entity]) -> torch.Tensor
Return a (len(entities), embedding_dim) L2-normalized embedding tensor.
Mixes mention-side and entity-side entities in one call by encoding each group with its own sub-encoder and reassembling the result in the original order -- callers (e.g. Trainer) never need to split a batch themselves.
Source code in src/linkingtk/algorithms/el/blink.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
BlinkLinker ¶
BlinkLinker(
mention_model_name: str = "distilbert-base-uncased",
entity_model_name: str | None = None,
embedding_dim: int = 256,
max_length: int = 96,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Bi-encoder Entity Linking scored by a two-tower BlinkEncoder.
Like ReFinEDLinker,
this has no fit() -- self.encoder is a plain
TrainableModel; train it via
Trainer directly, then call
link():
linker = BlinkLinker()
trainer = Trainer(model=linker.encoder, args=args, train_data=pairs, eval_data=eval_pairs)
trainer.train()
results = linker.link(mentions, kb_entries)
link() runs (just poorly) on an untrained encoder too -- there's
no _fitted flag, since nothing here can observe whether
Trainer.train() was ever called on self.encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mention_model_name
|
str
|
Forwarded to BlinkEncoder. |
'distilbert-base-uncased'
|
entity_model_name
|
str | None
|
Forwarded to |
None
|
embedding_dim
|
int
|
Forwarded to |
256
|
max_length
|
int
|
Forwarded to |
96
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/el/blink.py
230 231 232 233 234 235 236 237 238 239 240 241 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself
builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
Source code in src/linkingtk/algorithms/el/blink.py
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | |
ReFinEDEncoder ¶
ReFinEDEncoder(
model_name: str = "distilbert-base-uncased",
embedding_dim: int = 256,
max_length: int = 96,
)
Bases: Module
Shared/tied transformer bi-encoder for Entity Linking.
Satisfies TrainableModel:
encode() runs both mentions and KB entries through the same
transformer weights, distinguishing the two purely by how each
entity's text is formatted (mention-span markers vs. label+description)
-- ReFinED's real distinguishing idea versus BLINK's two independently
encoded sides.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name
|
str
|
A Hugging Face model id, loaded via
|
'distilbert-base-uncased'
|
embedding_dim
|
int
|
Output embedding dimension, independent of the backbone's native hidden size. |
256
|
max_length
|
int
|
Maximum token length per encoded text (truncated). |
96
|
Source code in src/linkingtk/algorithms/el/refined.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
encode ¶
encode(entities: list[Entity]) -> torch.Tensor
Return a (len(entities), embedding_dim) L2-normalized embedding tensor.
Source code in src/linkingtk/algorithms/el/refined.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
ReFinEDLinker ¶
ReFinEDLinker(
model_name: str = "distilbert-base-uncased",
embedding_dim: int = 256,
max_length: int = 96,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Bi-encoder Entity Linking scored by a shared/tied ReFinEDEncoder.
Unlike every other linker in this repo, this has no fit() --
self.encoder is a plain
TrainableModel; train it via
Trainer directly (matching
the documented Trainer(model=linker_model, ...)
interface), then call link():
linker = ReFinEDLinker()
trainer = Trainer(model=linker.encoder, args=args, train_data=pairs, eval_data=eval_pairs)
trainer.train()
results = linker.link(mentions, kb_entries)
link() runs (just poorly) on an untrained encoder too -- there's
no _fitted flag, since nothing here can observe whether
Trainer.train() was ever called on self.encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name
|
str
|
Forwarded to ReFinEDEncoder. |
'distilbert-base-uncased'
|
embedding_dim
|
int
|
Forwarded to |
256
|
max_length
|
int
|
Forwarded to |
96
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/el/refined.py
184 185 186 187 188 189 190 191 192 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself
builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
Source code in src/linkingtk/algorithms/el/refined.py
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | |
ReFinED¶
refined ¶
ReFinED-style bi-encoder Entity Linking.
ReFinED (Ayoola et al., "ReFinED: An Efficient Zero-shot-capable Approach to End-to-End Entity Linking", 2022, https://github.com/amazon-science/ReFinED) is impractical to wrap directly: it isn't on PyPI (GitHub-zip install only), is pinned to Python 3.8 (this repo requires >=3.10), and needs GB-scale Wikipedia model downloads on first use. This module instead builds a custom bi-encoder that follows ReFinED's real distinguishing architectural idea -- a single shared/tied transformer encoder that scores both the mention-in-context and the candidate-entity representation through the same weights (unlike BLINK's two-stage separate-encoder-plus-cross-encoder-reranker design) -- trained via Trainer, not the original package.
See ReFinEDEncoder and ReFinEDLinker.
ReFinEDEncoder ¶
ReFinEDEncoder(
model_name: str = "distilbert-base-uncased",
embedding_dim: int = 256,
max_length: int = 96,
)
Bases: Module
Shared/tied transformer bi-encoder for Entity Linking.
Satisfies TrainableModel:
encode() runs both mentions and KB entries through the same
transformer weights, distinguishing the two purely by how each
entity's text is formatted (mention-span markers vs. label+description)
-- ReFinED's real distinguishing idea versus BLINK's two independently
encoded sides.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name
|
str
|
A Hugging Face model id, loaded via
|
'distilbert-base-uncased'
|
embedding_dim
|
int
|
Output embedding dimension, independent of the backbone's native hidden size. |
256
|
max_length
|
int
|
Maximum token length per encoded text (truncated). |
96
|
Source code in src/linkingtk/algorithms/el/refined.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
encode ¶
encode(entities: list[Entity]) -> torch.Tensor
Return a (len(entities), embedding_dim) L2-normalized embedding tensor.
Source code in src/linkingtk/algorithms/el/refined.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
ReFinEDLinker ¶
ReFinEDLinker(
model_name: str = "distilbert-base-uncased",
embedding_dim: int = 256,
max_length: int = 96,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Bi-encoder Entity Linking scored by a shared/tied ReFinEDEncoder.
Unlike every other linker in this repo, this has no fit() --
self.encoder is a plain
TrainableModel; train it via
Trainer directly (matching
the documented Trainer(model=linker_model, ...)
interface), then call link():
linker = ReFinEDLinker()
trainer = Trainer(model=linker.encoder, args=args, train_data=pairs, eval_data=eval_pairs)
trainer.train()
results = linker.link(mentions, kb_entries)
link() runs (just poorly) on an untrained encoder too -- there's
no _fitted flag, since nothing here can observe whether
Trainer.train() was ever called on self.encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name
|
str
|
Forwarded to ReFinEDEncoder. |
'distilbert-base-uncased'
|
embedding_dim
|
int
|
Forwarded to |
256
|
max_length
|
int
|
Forwarded to |
96
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/el/refined.py
184 185 186 187 188 189 190 191 192 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself
builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
Source code in src/linkingtk/algorithms/el/refined.py
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | |
BLINK¶
blink ¶
BLINK-style bi-encoder Entity Linking.
BLINK (Wu et al., "Scalable Zero-shot Entity Linking with Dense Entity
Retrieval", 2020, https://github.com/facebookresearch/BLINK) is
impractical to wrap directly: it isn't on PyPI (GitHub-only), and its
release is pinned to old transformers/Python versions with GB-scale
Wikipedia model downloads. This module instead builds a custom bi-encoder
that follows BLINK's real distinguishing architectural idea -- two
independently-parameterized transformers, one for the mention-in-
context, one for the candidate entity's title+description, each scored
via [CLS]-pooled dot product (Section 4.1 of the paper) -- unlike
ReFinEDLinker's single
shared/tied encoder. Trained via
Trainer, not the original package.
Candidate generation (this module's link()) only covers BLINK's
first stage; the paper's second-stage cross-encoder reranker is out of
scope here (see issue #23).
See BlinkEncoder and BlinkLinker.
BlinkEncoder ¶
BlinkEncoder(
mention_model_name: str = "distilbert-base-uncased",
entity_model_name: str | None = None,
embedding_dim: int = 256,
max_length: int = 96,
encode_batch_size: int = 64,
)
Bases: Module
Two independently-parameterized transformer bi-encoder for Entity Linking.
Satisfies TrainableModel:
encode() dispatches each entity to a mention-side or entity-side
sub-transformer (by whether entity.context is not None, mirroring
ReFinEDEncoder's
dispatch) -- unlike ReFinEDEncoder, the two sides share no weights
at all, BLINK's real distinguishing idea.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mention_model_name
|
str
|
Hugging Face model id for the mention-side
encoder, loaded via |
'distilbert-base-uncased'
|
entity_model_name
|
str | None
|
Hugging Face model id for the entity-side
encoder. Defaults to the same as |
None
|
embedding_dim
|
int
|
Output embedding dimension for both sides, independent of either backbone's native hidden size. |
256
|
max_length
|
int
|
Maximum token length per encoded text (truncated). |
96
|
encode_batch_size
|
int
|
Maximum number of texts forwarded through a
sub-transformer in one go; |
64
|
Source code in src/linkingtk/algorithms/el/blink.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
encode ¶
encode(entities: list[Entity]) -> torch.Tensor
Return a (len(entities), embedding_dim) L2-normalized embedding tensor.
Mixes mention-side and entity-side entities in one call by encoding each group with its own sub-encoder and reassembling the result in the original order -- callers (e.g. Trainer) never need to split a batch themselves.
Source code in src/linkingtk/algorithms/el/blink.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
BlinkLinker ¶
BlinkLinker(
mention_model_name: str = "distilbert-base-uncased",
entity_model_name: str | None = None,
embedding_dim: int = 256,
max_length: int = 96,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
Bi-encoder Entity Linking scored by a two-tower BlinkEncoder.
Like ReFinEDLinker,
this has no fit() -- self.encoder is a plain
TrainableModel; train it via
Trainer directly, then call
link():
linker = BlinkLinker()
trainer = Trainer(model=linker.encoder, args=args, train_data=pairs, eval_data=eval_pairs)
trainer.train()
results = linker.link(mentions, kb_entries)
link() runs (just poorly) on an untrained encoder too -- there's
no _fitted flag, since nothing here can observe whether
Trainer.train() was ever called on self.encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mention_model_name
|
str
|
Forwarded to BlinkEncoder. |
'distilbert-base-uncased'
|
entity_model_name
|
str | None
|
Forwarded to |
None
|
embedding_dim
|
int
|
Forwarded to |
256
|
max_length
|
int
|
Forwarded to |
96
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/el/blink.py
230 231 232 233 234 235 236 237 238 239 240 241 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself
builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
Source code in src/linkingtk/algorithms/el/blink.py
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | |
Word Sense Disambiguation¶
wsd ¶
Word Sense Disambiguation (WSD) algorithms.
WSD identifies the relevant dictionary sense for a mention in context. Similar to EL, but the target dataset is a dictionary rather than a KB. Related systems: Lesk, EWISER, GlossBERT, ESC.
EwiserEncoder ¶
EwiserEncoder(
model_name_or_path: str = "bert-large-cased",
vocabulary: SenseVocabulary | None = None,
decoder_hidden_dim: int = 512,
adjacency: Tensor | None = None,
structured_logits_trainable: bool = True,
structured_logits_renormalize: bool = False,
freeze_encoder: bool = True,
dropout: float = 0.1,
num_summed_layers: int = 4,
max_length: int = 512,
forward_batch_size: int = 16,
output_embedding_init: Tensor | None = None,
)
Bases: Module
Frozen-BERT full-inventory sense classifier with a WordNet-graph output layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name_or_path
|
str
|
A Hugging Face encoder id, e.g.
|
'bert-large-cased'
|
vocabulary
|
SenseVocabulary | None
|
The SenseVocabulary the output layer classifies against. |
None
|
decoder_hidden_dim
|
int
|
Width of the decoder's single hidden layer.
Defaults to |
512
|
adjacency
|
Tensor | None
|
An optional sparse |
None
|
structured_logits_trainable
|
bool
|
Whether the graph's edge weights are updated during training. |
True
|
structured_logits_renormalize
|
bool
|
Forwarded to |
False
|
freeze_encoder
|
bool
|
Whether the BERT encoder's parameters are frozen.
Defaults to |
True
|
dropout
|
float
|
Decoder dropout rate. Checkpoint loading doesn't restore
this (dropout has no learned parameters), so it only affects
|
0.1
|
output_embedding_init
|
Tensor | None
|
An optional |
None
|
num_summed_layers
|
int
|
The encoder input is the sum of the last
|
4
|
max_length
|
int
|
Maximum subword sequence length per sentence (truncated). |
512
|
forward_batch_size
|
int
|
Number of distinct sentences |
16
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
Source code in src/linkingtk/algorithms/wsd/ewiser.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
from_checkpoint
classmethod
¶
from_checkpoint(
checkpoint_path: str | Path,
offsets_path: str | Path,
lexicon: str = "omw-en:1.4",
model_name_or_path: str = "bert-large-cased",
forward_batch_size: int = 16,
) -> EwiserEncoder
Load one of the three EWISER checkpoints released alongside the paper.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checkpoint_path
|
str | Path
|
Local path to a checkpoint file (e.g.
|
required |
offsets_path
|
str | Path
|
Local path to that checkpoint's own
|
required |
lexicon
|
str
|
Forwarded to |
'omw-en:1.4'
|
model_name_or_path
|
str
|
The encoder to pair with the checkpoint's
decoder weights. Defaults to |
'bert-large-cased'
|
forward_batch_size
|
int
|
Forwarded to |
16
|
Returns:
| Type | Description |
|---|---|
EwiserEncoder
|
An |
EwiserEncoder
|
|
EwiserEncoder
|
frozen encoder (never part of the checkpoint -- see the module |
EwiserEncoder
|
docstring). |
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
Source code in src/linkingtk/algorithms/wsd/ewiser.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
score ¶
score(pairs: list[tuple[Entity, Entity]]) -> torch.Tensor
Return a (len(pairs),) tensor: each pair's raw sense-classification logit.
Unlike a cross-encoder's score, this does not run one
forward pass per pair. It groups pairs by each mention's unique
sentence (see mention_sentence_and_span), runs the
encoder+decoder once per distinct sentence (batched
forward_batch_size sentences at a time), and reads each pair's
score off as logits[mention_word_index, candidate_sense_index]
from that sentence's shared output. A candidate whose sense id
isn't in vocabulary, or a mention whose span doesn't resolve to
a word position, scores -inf (never spuriously wins a match).
Each chunk's [num_words, len(vocabulary)] logits are consumed
(scores extracted) and released before the next chunk is encoded,
rather than holding every distinct sentence's full logits matrix
in memory for the whole call -- at len(vocabulary) ~117k, a
large evaluation set's worth of these held simultaneously
(thousands of sentences at once, e.g. UFSAC's "ALL" split) is
enough to exhaust even a 24GB GPU; confirmed by hitting exactly
that CUDA out of memory running examples/ewiser_reproduction.py
before this was fixed.
Source code in src/linkingtk/algorithms/wsd/ewiser.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | |
EwiserLinker ¶
EwiserLinker(
model: EwiserEncoder,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
WSD linking scored by an EwiserEncoder.
Unlike GlossBertLinker,
this takes an already-constructed EwiserEncoder rather than a bare
model_name_or_path string -- EwiserEncoder needs a SenseVocabulary
(and usually an adjacency) at construction time, with no sane
string-only default.
encoder = EwiserEncoder.from_checkpoint("ewiser.semcor.pt", "offsets.txt")
linker = EwiserLinker(encoder)
results = linker.link(mentions, senses)
Or, for from-scratch/continued training, construct EwiserEncoder
directly and train it via
EwiserTrainer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
EwiserEncoder
|
The |
required |
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/wsd/ewiser.py
419 420 421 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
If dataset2 is a WnEntitySource whose lexicon differs from
model.vocabulary's own, candidate ids are translated into the
vocabulary's lexicon via
synset_id_via_ili before
scoring (see _translate_for_scoring's own docstring) -- without
this, every candidate would fail
SenseVocabulary.index_for
and silently score -inf (#67). Candidate ids in the returned
dict are still dataset2's own (untranslated) ids -- the
translation is scoring-only, invisible to the caller.
Source code in src/linkingtk/algorithms/wsd/ewiser.py
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | |
GlossBertEncoder ¶
GlossBertEncoder(
model_name_or_path: str = "bert-base-uncased",
max_length: int = 512,
forward_batch_size: int = 32,
)
Bases: Module
BertForSequenceClassification-based cross-encoder scorer for WSD.
Satisfies CrossEncoderModel.
Unlike ReFinEDEncoder/
BlinkEncoder (generic
AutoModel, any backbone), this is hardcoded to BERT -- GlossBERT's
own reference implementation is BERT-specific, and this is exactly
what lets model_name_or_path point at the paper's own published
checkpoint directory as well as a fresh bert-base-uncased.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name_or_path
|
str
|
A Hugging Face model id, or a local directory
(e.g. the paper's published |
'bert-base-uncased'
|
max_length
|
int
|
Maximum token length for the tokenized |
512
|
forward_batch_size
|
int
|
Chunk size |
32
|
Source code in src/linkingtk/algorithms/wsd/glossbert.py
141 142 143 144 145 146 147 148 149 150 151 152 153 | |
from_checkpoint
classmethod
¶
from_checkpoint(
checkpoint_path: str | Path,
max_length: int = 512,
forward_batch_size: int = 32,
) -> GlossBertEncoder
Load the paper's own published checkpoint directory (e.g. Sent_CLS_WS, extracted).
Same entry point name as
EwiserEncoder.from_checkpoint/
EscEncoder.from_checkpoint,
for a consistent checkpoint-loading interface across WSD linkers
(issue #64) -- unlike those two, no format translation is needed
here (the paper's checkpoint is already a plain Hugging Face
config.json/pytorch_model.bin/vocab.txt directory, exactly
what model_name_or_path already accepts), so this is a thin
wrapper around the constructor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checkpoint_path
|
str | Path
|
Local path to the extracted checkpoint directory -- not bundled with this package. |
required |
max_length
|
int
|
Forwarded to |
512
|
forward_batch_size
|
int
|
Forwarded to |
32
|
Returns:
| Type | Description |
|---|---|
GlossBertEncoder
|
A |
Source code in src/linkingtk/algorithms/wsd/glossbert.py
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
score ¶
score(pairs: list[tuple[Entity, Entity]]) -> torch.Tensor
Return a (len(pairs),) tensor: the class-1-minus-class-0 logit margin per pair.
Source code in src/linkingtk/algorithms/wsd/glossbert.py
189 190 191 192 193 194 195 | |
GlossBertLinker ¶
GlossBertLinker(
model_name_or_path: str = "bert-base-uncased",
max_length: int = 512,
forward_batch_size: int = 32,
matching: Matcher = DEFAULT_MATCHER,
model: GlossBertEncoder | None = None,
)
Bases: BaseLinker
WSD linking scored by a GlossBertEncoder.
Unlike every other linker in this repo, this has no fit() --
self.model is a plain
CrossEncoderModel;
train it via CrossEncoderTrainer
directly, then call link():
linker = GlossBertLinker()
trainer = CrossEncoderTrainer(model=linker.model, args=args, train_data=pairs)
trainer.train()
results = linker.link(mentions, senses)
Or skip training entirely and load the paper's own published
checkpoint, either directly (GlossBertLinker(model_name_or_path="/path/to/Sent_CLS_WS"))
or, matching EwiserLinker/
EscLinker's pattern, by
passing an already-built encoder:
encoder = GlossBertEncoder.from_checkpoint("/path/to/Sent_CLS_WS")
linker = GlossBertLinker(model=encoder)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name_or_path
|
str
|
Forwarded to
GlossBertEncoder.
Ignored if |
'bert-base-uncased'
|
max_length
|
int
|
Forwarded to |
512
|
forward_batch_size
|
int
|
Forwarded to |
32
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
model
|
GlossBertEncoder | None
|
An already-built |
None
|
Source code in src/linkingtk/algorithms/wsd/glossbert.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
Source code in src/linkingtk/algorithms/wsd/glossbert.py
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | |
LeskLinker ¶
LeskLinker()
Bases: StringSimilarityLinker
WSD linker that scores candidate senses by context/gloss word overlap.
A preconfigured
StringSimilarityLinker
(source_field="context", target_field="description",
metric="word_overlap") — see that class for the ranking and
alternatives behavior. Kept as its own named class since Lesk is a
well-known algorithm in its own right.
Source code in src/linkingtk/algorithms/wsd/lesk.py
26 27 | |
lesk ¶
Lesk-style Word Sense Disambiguation linker.
References
Lesk, M. (1986). Automatic sense disambiguation using machine readable dictionaries: how to tell a pine cone from an ice cream cone. In Proceedings of the 5th annual international conference on Systems documentation (SIGDOC '86).
LeskLinker ¶
LeskLinker()
Bases: StringSimilarityLinker
WSD linker that scores candidate senses by context/gloss word overlap.
A preconfigured
StringSimilarityLinker
(source_field="context", target_field="description",
metric="word_overlap") — see that class for the ranking and
alternatives behavior. Kept as its own named class since Lesk is a
well-known algorithm in its own right.
Source code in src/linkingtk/algorithms/wsd/lesk.py
26 27 | |
GlossBERT¶
glossbert ¶
GlossBERT Word Sense Disambiguation.
GlossBERT (Huang et al., "GlossBERT: BERT for Word Sense Disambiguation
with Gloss Knowledge", EMNLP 2019,
https://github.com/HSLCY/GlossBERT) is a true cross-encoder: it
concatenates a mention's context sentence and a candidate sense's gloss
into one BERT input ([CLS] context [SEP] gloss [SEP]) and classifies
the pair as matching or not, unlike
ReFinEDLinker/
BlinkLinker's bi-encoders
(independently encode() each side, then compare embeddings). Trained
via CrossEncoderTrainer,
built specifically for this shape.
This implements the paper's best-reported configuration,
GlossBERT(Sent-CLS-WS), ported directly from the reference
implementation's own data-preparation code
(preparation/generate_sent_cls_ws.py), not just the paper text:
- Candidates are every WordNet sense of the mention's surface lemma,
across every part of speech (not filtered to the mention's own gold
POS) -- matches this repo's existing
ExactMatch/EntitySource.search()design already. - The gloss side of each pair is
f"{mention_surface_text} : {gloss}"-- the mention's own surface form, not the sense's dictionary lemma, confirmed against the reference'swordnet/index.sense.gloss. - "Weak supervision" (the
-WSin the paper's best model) wraps the mention span in the context sentence with literal"quote characters -- not special tokens. Confirmed by the paper's own published checkpoint (state dictbert.*+classifier.weight (2, 768), vocab size 30522 -- stockbert-base-uncased, nothing added). - The classification head is
BertForSequenceClassification-shaped (num_labels=2,[CLS]-pooled, softmax cross-entropy in the original) --score()returns the class-1-minus-class-0 logit margin, which is mathematically identical to that 2-class softmax cross-entropy (so CrossEncoderTrainer's generic binary-cross-entropy-on-the-margin loss trains it exactly the same way, without a GlossBERT-specific loss function).
Because the architecture matches exactly, GlossBertEncoder also loads
the paper's own published checkpoint directly via
GlossBertEncoder.from_checkpoint (same entry point name as
EwiserEncoder.from_checkpoint/
EscEncoder.from_checkpoint,
issue #64) -- see examples/glossbert_reproduction.py for a real
evaluation against it.
See GlossBertEncoder and GlossBertLinker.
GlossBertEncoder ¶
GlossBertEncoder(
model_name_or_path: str = "bert-base-uncased",
max_length: int = 512,
forward_batch_size: int = 32,
)
Bases: Module
BertForSequenceClassification-based cross-encoder scorer for WSD.
Satisfies CrossEncoderModel.
Unlike ReFinEDEncoder/
BlinkEncoder (generic
AutoModel, any backbone), this is hardcoded to BERT -- GlossBERT's
own reference implementation is BERT-specific, and this is exactly
what lets model_name_or_path point at the paper's own published
checkpoint directory as well as a fresh bert-base-uncased.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name_or_path
|
str
|
A Hugging Face model id, or a local directory
(e.g. the paper's published |
'bert-base-uncased'
|
max_length
|
int
|
Maximum token length for the tokenized |
512
|
forward_batch_size
|
int
|
Chunk size |
32
|
Source code in src/linkingtk/algorithms/wsd/glossbert.py
141 142 143 144 145 146 147 148 149 150 151 152 153 | |
from_checkpoint
classmethod
¶
from_checkpoint(
checkpoint_path: str | Path,
max_length: int = 512,
forward_batch_size: int = 32,
) -> GlossBertEncoder
Load the paper's own published checkpoint directory (e.g. Sent_CLS_WS, extracted).
Same entry point name as
EwiserEncoder.from_checkpoint/
EscEncoder.from_checkpoint,
for a consistent checkpoint-loading interface across WSD linkers
(issue #64) -- unlike those two, no format translation is needed
here (the paper's checkpoint is already a plain Hugging Face
config.json/pytorch_model.bin/vocab.txt directory, exactly
what model_name_or_path already accepts), so this is a thin
wrapper around the constructor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checkpoint_path
|
str | Path
|
Local path to the extracted checkpoint directory -- not bundled with this package. |
required |
max_length
|
int
|
Forwarded to |
512
|
forward_batch_size
|
int
|
Forwarded to |
32
|
Returns:
| Type | Description |
|---|---|
GlossBertEncoder
|
A |
Source code in src/linkingtk/algorithms/wsd/glossbert.py
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
score ¶
score(pairs: list[tuple[Entity, Entity]]) -> torch.Tensor
Return a (len(pairs),) tensor: the class-1-minus-class-0 logit margin per pair.
Source code in src/linkingtk/algorithms/wsd/glossbert.py
189 190 191 192 193 194 195 | |
GlossBertLinker ¶
GlossBertLinker(
model_name_or_path: str = "bert-base-uncased",
max_length: int = 512,
forward_batch_size: int = 32,
matching: Matcher = DEFAULT_MATCHER,
model: GlossBertEncoder | None = None,
)
Bases: BaseLinker
WSD linking scored by a GlossBertEncoder.
Unlike every other linker in this repo, this has no fit() --
self.model is a plain
CrossEncoderModel;
train it via CrossEncoderTrainer
directly, then call link():
linker = GlossBertLinker()
trainer = CrossEncoderTrainer(model=linker.model, args=args, train_data=pairs)
trainer.train()
results = linker.link(mentions, senses)
Or skip training entirely and load the paper's own published
checkpoint, either directly (GlossBertLinker(model_name_or_path="/path/to/Sent_CLS_WS"))
or, matching EwiserLinker/
EscLinker's pattern, by
passing an already-built encoder:
encoder = GlossBertEncoder.from_checkpoint("/path/to/Sent_CLS_WS")
linker = GlossBertLinker(model=encoder)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name_or_path
|
str
|
Forwarded to
GlossBertEncoder.
Ignored if |
'bert-base-uncased'
|
max_length
|
int
|
Forwarded to |
512
|
forward_batch_size
|
int
|
Forwarded to |
32
|
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
model
|
GlossBertEncoder | None
|
An already-built |
None
|
Source code in src/linkingtk/algorithms/wsd/glossbert.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
Source code in src/linkingtk/algorithms/wsd/glossbert.py
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | |
EWISER¶
ewiser ¶
EWISER Word Sense Disambiguation.
EWISER (Bevilacqua & Navigli, "Breaking Through the 80% Glass Ceiling:
Raising the State of the Art in Word Sense Disambiguation by Incorporating
Knowledge Graph Information", ACL 2020,
https://github.com/SapienzaNLP/ewiser) is a full-inventory sense
classifier, architecturally very different from
GlossBertLinker's
pairwise cross-encoder: it encodes a whole sentence once with a
frozen BERT, projects every word's hidden state through a small FFN to
raw logits over the entire WordNet sense inventory, then applies one
linear WordNet-relation graph-propagation step to those logits (see
StructuredLogits)
before a mention's candidate senses are read off as indices into that one
shared output vector -- there's no gloss text involved in the forward pass
at all. EwiserEncoder.score(pairs) still returns the same
(len(pairs),) -> torch.Tensor shape
GlossBertEncoder.score
does (so EwiserLinker's link()
can reuse the same blocking -> score -> group -> match pattern every
linker in this package uses), but computes it very differently internally
-- see EwiserEncoder.score's own docstring.
Reimplemented from the paper and from the tensor shapes/keys observed directly in the three checkpoints released alongside it (not ported from EWISER's own source, which is CC-BY-NC-SA 4.0 licensed -- incompatible with redistributing literal code under this package's Apache-2.0 license). Confirmed by reading the reference's own model/criterion code directly (not guessed):
- The BERT encoder is never fine-tuned -- every released checkpoint's
own state dict holds only 11 tensors, all in the small decoder (2-layer
FFN + the graph module); the encoder's weights come straight from a
frozen
bert-large-cased, loaded fresh from Hugging Face, not from the checkpoint. - The encoder input is the sum of the last 4 BERT hidden-state layers
(
context_embeddings_layers=[-4,-3,-2,-1]), not justlast_hidden_state-- despite every released checkpoint's own recordedcontext_embeddings_use_all_hidden=False, which would suggest otherwise. Confirmed this flag is dead: the reference's ownTaggerModel.build_modelcomputesuse_all_hiddenfromlen(args.context_embeddings_type) > 1(a variable-name mix-up --context_embeddings_typeis the string"bert", not the actual layer list), which for"bert"(length 4) is alwaysTrue, unconditionally. SeeEwiserEncoder's ownnum_summed_layersdocstring for how this was traced. - Each whitespace-split word is subword-tokenized in isolation via a
from-scratch WordPiece implementation
(wordpiece_tokenize_words),
not a single
tokenizer(words, is_split_into_words=True)call -- confirmed, by tracing a real checkpoint-reproduction gap layer by layer down to one divergent subtoken, thatis_split_into_words=Truestill runs BERT's punctuation-splitting pre-tokenizer on each word first (splitting e.g. UFSAC's own single-token"Oct."into separate"Oct"/"."pre-tokens before wordpiece), producing a standalone"."token where EWISER's own reference (wordpiece-tokenizing the literal string"Oct."directly, no pre-splitting) produces a"##."continuation piece instead -- a different vocabulary entry with an unrelated embedding. Seewordpiece_tokenize_words's own docstring for the full diagnosis. EwiserEncoder.from_checkpointreads a checkpoint's own baked-in sparse WordNet adjacency directly from its state dict -- no graph construction needed for checkpoint-based inference (see build_relation_adjacency for the from-scratch-training path that does need one). Unpickling a checkpoint file requires tolerating references to training-only classes from its original (pre-release) package name,qbert-- see load_fairseq_checkpoint.
See EwiserEncoder and EwiserLinker.
EwiserEncoder ¶
EwiserEncoder(
model_name_or_path: str = "bert-large-cased",
vocabulary: SenseVocabulary | None = None,
decoder_hidden_dim: int = 512,
adjacency: Tensor | None = None,
structured_logits_trainable: bool = True,
structured_logits_renormalize: bool = False,
freeze_encoder: bool = True,
dropout: float = 0.1,
num_summed_layers: int = 4,
max_length: int = 512,
forward_batch_size: int = 16,
output_embedding_init: Tensor | None = None,
)
Bases: Module
Frozen-BERT full-inventory sense classifier with a WordNet-graph output layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name_or_path
|
str
|
A Hugging Face encoder id, e.g.
|
'bert-large-cased'
|
vocabulary
|
SenseVocabulary | None
|
The SenseVocabulary the output layer classifies against. |
None
|
decoder_hidden_dim
|
int
|
Width of the decoder's single hidden layer.
Defaults to |
512
|
adjacency
|
Tensor | None
|
An optional sparse |
None
|
structured_logits_trainable
|
bool
|
Whether the graph's edge weights are updated during training. |
True
|
structured_logits_renormalize
|
bool
|
Forwarded to |
False
|
freeze_encoder
|
bool
|
Whether the BERT encoder's parameters are frozen.
Defaults to |
True
|
dropout
|
float
|
Decoder dropout rate. Checkpoint loading doesn't restore
this (dropout has no learned parameters), so it only affects
|
0.1
|
output_embedding_init
|
Tensor | None
|
An optional |
None
|
num_summed_layers
|
int
|
The encoder input is the sum of the last
|
4
|
max_length
|
int
|
Maximum subword sequence length per sentence (truncated). |
512
|
forward_batch_size
|
int
|
Number of distinct sentences |
16
|
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
Source code in src/linkingtk/algorithms/wsd/ewiser.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
from_checkpoint
classmethod
¶
from_checkpoint(
checkpoint_path: str | Path,
offsets_path: str | Path,
lexicon: str = "omw-en:1.4",
model_name_or_path: str = "bert-large-cased",
forward_batch_size: int = 16,
) -> EwiserEncoder
Load one of the three EWISER checkpoints released alongside the paper.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checkpoint_path
|
str | Path
|
Local path to a checkpoint file (e.g.
|
required |
offsets_path
|
str | Path
|
Local path to that checkpoint's own
|
required |
lexicon
|
str
|
Forwarded to |
'omw-en:1.4'
|
model_name_or_path
|
str
|
The encoder to pair with the checkpoint's
decoder weights. Defaults to |
'bert-large-cased'
|
forward_batch_size
|
int
|
Forwarded to |
16
|
Returns:
| Type | Description |
|---|---|
EwiserEncoder
|
An |
EwiserEncoder
|
|
EwiserEncoder
|
frozen encoder (never part of the checkpoint -- see the module |
EwiserEncoder
|
docstring). |
Raises:
| Type | Description |
|---|---|
LinkingTKError
|
If |
Source code in src/linkingtk/algorithms/wsd/ewiser.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
score ¶
score(pairs: list[tuple[Entity, Entity]]) -> torch.Tensor
Return a (len(pairs),) tensor: each pair's raw sense-classification logit.
Unlike a cross-encoder's score, this does not run one
forward pass per pair. It groups pairs by each mention's unique
sentence (see mention_sentence_and_span), runs the
encoder+decoder once per distinct sentence (batched
forward_batch_size sentences at a time), and reads each pair's
score off as logits[mention_word_index, candidate_sense_index]
from that sentence's shared output. A candidate whose sense id
isn't in vocabulary, or a mention whose span doesn't resolve to
a word position, scores -inf (never spuriously wins a match).
Each chunk's [num_words, len(vocabulary)] logits are consumed
(scores extracted) and released before the next chunk is encoded,
rather than holding every distinct sentence's full logits matrix
in memory for the whole call -- at len(vocabulary) ~117k, a
large evaluation set's worth of these held simultaneously
(thousands of sentences at once, e.g. UFSAC's "ALL" split) is
enough to exhaust even a 24GB GPU; confirmed by hitting exactly
that CUDA out of memory running examples/ewiser_reproduction.py
before this was fixed.
Source code in src/linkingtk/algorithms/wsd/ewiser.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | |
EwiserLinker ¶
EwiserLinker(
model: EwiserEncoder,
matching: Matcher = DEFAULT_MATCHER,
)
Bases: BaseLinker
WSD linking scored by an EwiserEncoder.
Unlike GlossBertLinker,
this takes an already-constructed EwiserEncoder rather than a bare
model_name_or_path string -- EwiserEncoder needs a SenseVocabulary
(and usually an adjacency) at construction time, with no sane
string-only default.
encoder = EwiserEncoder.from_checkpoint("ewiser.semcor.pt", "offsets.txt")
linker = EwiserLinker(encoder)
results = linker.link(mentions, senses)
Or, for from-scratch/continued training, construct EwiserEncoder
directly and train it via
EwiserTrainer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
EwiserEncoder
|
The |
required |
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/wsd/ewiser.py
419 420 421 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
If dataset2 is a WnEntitySource whose lexicon differs from
model.vocabulary's own, candidate ids are translated into the
vocabulary's lexicon via
synset_id_via_ili before
scoring (see _translate_for_scoring's own docstring) -- without
this, every candidate would fail
SenseVocabulary.index_for
and silently score -inf (#67). Candidate ids in the returned
dict are still dataset2's own (untranslated) ids -- the
translation is scoring-only, invisible to the caller.
Source code in src/linkingtk/algorithms/wsd/ewiser.py
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | |
_ewiser_vocab ¶
Sense vocabulary: index <-> synset id, for EWISER's full-inventory output layer.
EwiserEncoder classifies
every word against a fixed, closed vocabulary of WordNet senses rather than
scoring one candidate at a time (unlike
GlossBertEncoder's
cross-encoder). SenseVocabulary is that fixed index space.
Two construction paths, since the index order matters differently depending on where it comes from:
from_offsets_fileloads EWISER's own published checkpoint dictionary (frequency-sorted over the original authors' training corpus) -- this order is not reproducible from first principles (it depends on corpus statistics we don't have), so checkpoint-compatible inference requires the literal file, supplied locally by the caller (mirrors howexamples/glossbert_reproduction.pyrequires a local copy of GlossBERT's own published checkpoint directory -- neither file is redistributed by this package).from_wnbuilds a fresh vocabulary for from-scratch training, where any consistent order works -- nothing downstream depends on matching upstream's specific index assignment.
SenseVocabulary ¶
SenseVocabulary(
index_to_synset_id: list[str | None],
lexicon: str | None = None,
)
index <-> synset id mapping for an EwiserEncoder's output layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_to_synset_id
|
list[str | None]
|
The vocabulary itself, in index order. An entry
of |
required |
lexicon
|
str | None
|
The |
None
|
Source code in src/linkingtk/algorithms/wsd/_ewiser_vocab.py
51 52 53 54 55 56 57 58 | |
from_offsets_file
classmethod
¶
from_offsets_file(
path: str | Path,
lexicon: str = "omw-en:1.4",
nspecial: int = 4,
) -> SenseVocabulary
Load EWISER's own res/dictionaries/offsets.txt.
Each line is "<token> <frequency>"; token is either a
WordNet 3.0 offset ("wn:02084071n") or a non-offset dummy
symbol (e.g. fairseq's "madeupword0000" padding entry), which
becomes a reserved (None) slot like the nspecial prefix.
Offsets that don't resolve in lexicon (version skew) also become
None rather than raising, since a handful of unresolvable
entries shouldn't block loading the other ~117,000.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Local path to |
required |
lexicon
|
str
|
Passed to wn30_offset_to_synset_id. |
'omw-en:1.4'
|
nspecial
|
int
|
Reserved slots prepended before the file's own
entries, matching fairseq's convention ( |
4
|
Returns:
| Type | Description |
|---|---|
SenseVocabulary
|
A |
SenseVocabulary
|
matching a published checkpoint's |
SenseVocabulary
|
row count (117664 for the three EWISER checkpoints released |
SenseVocabulary
|
alongside the paper). |
Source code in src/linkingtk/algorithms/wsd/_ewiser_vocab.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
from_wn
classmethod
¶
from_wn(
synset_ids: list[str], nspecial: int = 4
) -> SenseVocabulary
Build a fresh vocabulary for from-scratch training.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
synset_ids
|
list[str]
|
The senses to index, e.g. every synset observed in a training corpus. Deduplicated and sorted for a deterministic (order-independent-correctness) index assignment. |
required |
nspecial
|
int
|
Reserved slots prepended before the real entries. |
4
|
Returns:
| Type | Description |
|---|---|
SenseVocabulary
|
A |
Source code in src/linkingtk/algorithms/wsd/_ewiser_vocab.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
index_for ¶
index_for(synset_id: str) -> int | None
The output-layer index for synset_id, or None if it's not in this vocabulary.
A synset_id from the wrong lexicon silently returns None
just like a genuinely absent one -- there's no way to tell "not in
this WordNet's sense inventory" apart from "belongs to a
different, non-overlapping id space" from a string lookup alone
(confirmed real: from_offsets_file's default lexicon is
"omw-en:1.4", whose ids look like "omw-en-00319912-v", but
WnEntitySource's own default is "oewn:2021", whose ids look
like "oewn-00319912-v" -- and per
synset_id_to_wn30_offset's
own docstring, oewn:*'s offsets have diverged from PWN 3.0, so
even the numeric part rarely matches). Left to itself, every
candidate from a mismatched WnEntitySource would fail this
lookup, and EwiserEncoder.score would score every one of them
-inf -- silently, with no exception (see #67). EwiserLinker
avoids this by translating a WnEntitySource candidate's id into
self.lexicon's id space via
synset_id_via_ili before
it ever reaches this method, whenever the two lexicons differ --
see EwiserLinker.score_candidates's own docstring.
Source code in src/linkingtk/algorithms/wsd/_ewiser_vocab.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
synset_id_for ¶
synset_id_for(index: int) -> str | None
The synset id at index, or None for a reserved/unresolved slot.
Source code in src/linkingtk/algorithms/wsd/_ewiser_vocab.py
141 142 143 | |
_ewiser_graph ¶
WordNet relation-graph construction for StructuredLogits.
Builds a sparse [V, V] adjacency over a
SenseVocabulary
from wn's own hypernym/similar/verb_group/derivation
relations -- confirmed live against this repo's own installed lexicon that
all four are real, populated relation types (derivation is sense-level;
the other three are synset-level).
Reimplements the algorithm observed in EWISER's own graph builder
(ResourceManager.make_adjacency_from_files), not its code (that source is
CC-BY-NC-SA licensed, not redistributed here): for every node, gather every
incoming edge, normalize their weights to sum to 1 over all predecessors
first, then truncate to the top max_incoming by weight -- in that
order. The surviving top-k weights generally do not re-sum to 1, which
is a real, confirmed property of the reference's own behavior, not a bug
in this reimplementation.
Edge direction matches the reference: a hypernym relation edge is added
(hyponym_index, hypernym_index) -- the more general (and typically
better-attested) hypernym's logit propagates down into its more specific
hyponym, letting the model transfer confidence to senses it saw less
often in training. similar/verb_group/derivation are only added in
the direction each call reports -- confirmed directly that wn already
stores all three reciprocally (both synsets/senses on a pair list each
other), so the reverse direction is picked up for free when the loop
below reaches the other node; adding it a second time here would double
the edge weight.
Sourced natively via wn rather than EWISER's own BabelNet-id-keyed
res/edges/*.tsv files, avoiding both a heavy extra resource dependency
and the bnids_map.txt remapping step those files require. This means a
from-scratch graph cannot be verified bit-exactly against EWISER's own
(different relation coverage, different tie-breaking order) -- only
structurally, on synthetic data (see tests/algorithms/wsd/test_ewiser_graph.py).
Only relevant for
EwiserTrainer's
from-scratch/continued-training path -- checkpoint-based inference uses
the checkpoint's own already-baked-in adjacency instead (see
EwiserEncoder.from_checkpoint).
build_relation_adjacency ¶
build_relation_adjacency(
vocabulary: SenseVocabulary,
lexicon: str = "omw-en:1.4",
max_incoming: int = 5,
) -> torch.Tensor
Build a sparse [len(vocabulary), len(vocabulary)] WordNet relation adjacency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vocabulary
|
SenseVocabulary
|
The sense inventory to build edges over -- only
relation targets that are themselves in |
required |
lexicon
|
str
|
|
'omw-en:1.4'
|
max_incoming
|
int
|
Maximum incoming edges kept per node, by normalized weight (see module docstring for the normalize-then-truncate order). |
5
|
Returns:
| Type | Description |
|---|---|
Tensor
|
A coalesced sparse COO tensor, |
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/algorithms/wsd/_ewiser_graph.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
_ewiser_structured_logits ¶
One linear WordNet-relation graph-propagation step over sense logits.
EWISER's central idea (Bevilacqua & Navigli, ACL 2020): instead of scoring
each sense independently, boost every synset's raw classification logit by
a weighted sum of its WordNet-neighbors' logits, so the model can transfer
confidence from a frequently-labeled sense (e.g. a common hypernym) to a
rarer one it's graph-adjacent to, even if that rarer sense was never seen
labeled in training. This module reimplements that operation --
new_logits = (A @ old_logits) + old_logits for a sparse [V, V]
adjacency A -- from the paper's description and the shape observed
directly in the released checkpoints' state dicts, not ported from
EWISER's own (CC-BY-NC-SA licensed) source.
Deliberately reimplemented with plain torch.sparse.mm/
torch.sparse_coo_tensor instead of the reference's own
torch_scatter/torch_sparse dependency: those packages are
unconditionally imported by the reference's equivalent module for what is,
in substance, one sparse-dense matmul plus an optional row-sum
normalization -- and they're notoriously fragile, tightly pinned to exact
torch+CUDA build combinations. A deliberate deviation from upstream, not
an oversight.
The trainable=True, renormalize=False path (the default, and what
every released checkpoint and the reference's own bin/train-ewiser.sh
actually use in both training stages) runs through _SparsePropagate
below instead of a bare torch.sparse.mm call: torch.sparse.mm's
default backward, when the sparse operand itself requires grad,
materializes a dense [V, V] gradient rather than an O(nnz) one --
invisible at a toy vocabulary size, but confirmed directly (issue #58) to
OOM at EWISER's own real ~117,664-entry WordNet vocabulary (a dense
[117664, 117664] float32 gradient is ~51.6GB). _SparsePropagate
computes the same values via an explicit, O(nnz) indexed
gather-multiply-sum instead, verified against torch.autograd.gradcheck
and a dense reference implementation, and measured at 3ms/0.4GB peak at
the full ~117,664-entry / ~168K-edge scale (vs. an unfixed OOM). The
renormalize=True combination is untouched (no released checkpoint or
bin/train-ewiser.sh config uses it, so it isn't scale-verified the way
the default path now is) and still goes through the original
torch.sparse.mm path.
StructuredLogits ¶
StructuredLogits(
adjacency: Tensor,
trainable: bool = False,
renormalize: bool = False,
)
Bases: Module
Graph-propagation step: new_logits = (A @ old_logits) + old_logits.
Stores the sparse adjacency's indices/values/size as separate
parameters under the same names
(adjacency_pars.0/adjacency_pars.1/adjacency_pars.2) the
released EWISER checkpoints use for their own equivalent module, so a
checkpoint's state dict loads via plain load_state_dict(strict=True)
with no key renaming.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
adjacency
|
Tensor
|
A sparse (coalesced or not) |
required |
trainable
|
bool
|
Whether the adjacency's edge weights are updated during training (the indices/shape are always fixed). |
False
|
renormalize
|
bool
|
If |
False
|
Source code in src/linkingtk/algorithms/wsd/_ewiser_structured_logits.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | |
forward ¶
forward(logits: Tensor) -> torch.Tensor
Propagate logits ([..., V]) one step through the WordNet graph.
Source code in src/linkingtk/algorithms/wsd/_ewiser_structured_logits.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | |
_ewiser_decoder ¶
The trainable EWISER decoder head, and released-checkpoint loading.
Split out of ewiser.py to keep that module focused on the public
EwiserEncoder/EwiserLinker API (see this package's ~300-line-per-file
convention).
EwiserDecoder ¶
EwiserDecoder(
input_dim: int,
hidden_dim: int,
vocab_size: int,
adjacency: Tensor | None = None,
structured_logits_trainable: bool = True,
structured_logits_renormalize: bool = False,
dropout: float = 0.1,
)
Bases: Module
The small trainable head: BatchNorm -> Linear -> swish -> Linear + graph conv.
Submodule names (norm, linears, logits, structured_logits)
match the released checkpoints' own state dict keys exactly (confirmed
directly against a real checkpoint file), so a checkpoint's decoder
weights load via plain load_state_dict(strict=True).
Source code in src/linkingtk/algorithms/wsd/_ewiser_decoder.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
load_fairseq_checkpoint ¶
load_fairseq_checkpoint(path: str | Path) -> dict[str, Any]
Load a released EWISER .pt checkpoint file.
Its pickle stream references training-only bookkeeping classes from
its original (pre-release) package name, qbert, and from
fairseq (e.g. qbert.fairseq_ext.meters.SumMeter,
fairseq.meters.AverageMeter) -- none of which this package depends
on or needs (only data["model"]'s plain tensors and data["args"]'s
plain argparse.Namespace are used), but standard unpickling fails the
entire load if even one referenced class can't be resolved. Works
around this with a permissive Unpickler.find_class that substitutes
an inert placeholder class for anything unresolvable instead of
failing -- confirmed safe by inspecting exactly which classes get
requested against a real checkpoint file (only optimizer/meter
training-history objects, never anything under data["model"]).
Source code in src/linkingtk/algorithms/wsd/_ewiser_decoder.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
_ewiser_sense_embeddings ¶
Pretrained sense-embedding loaders for EwiserEncoder's output layer.
EWISER's own strongest results (Section 4.2, "Output Embeddings", of the
paper) come from initializing decoder.logits.weight -- the sense output
embedding layer -- from off-the-shelf sense vectors rather than a random
init, then optionally freezing it for a few epochs
(EwiserTrainer.freeze_output_epochs).
Two off-the-shelf vector sets are named in the paper: LMMS 2048d
(Loureiro and Jorge, 2019) and SensEmBERT+LMMS (Scarlini et al., 2020,
enhancing LMMS with BabelNet+Wikipedia for nouns, backed off to LMMS for
verbs/adjectives/adverbs -- SensEmBERT's own vectors are "in the same
space as LMMS", per the paper). The paper's own recorded pipeline for
both (confirmed directly from its Section 4.2 text, not from the
reference's CC-BY-NC-SA-licensed scripts): normalize each raw sense
vector to unit length, take the centroid of every sense in a synset,
reduce to 512d with truncated SVD, normalize again.
Two loaders, for two different starting points:
load_synset_centroid_vectorsloads an already-built, already-reduced synset-vector file -- e.g. the literalres/embeddings/sensembert+lmms.svd512.synset-centroid.vecthe reference's own training script uses (confirmed againstbin/train-ewiser.sh's$EMBEDDINGSvariable), a GloVe-style file keyed by WordNet 3.0 offset tokens ("wn:02084071n", same conventionSenseVocabulary.from_offsets_filealready parses). This is the fastest path to the paper's own best configuration when such a file is available -- no SVD or centroiding needed at load time.build_synset_centroid_vectors_from_lmmsbuilds the same shape of tensor from LMMS's own raw, sensekey-keyed release files (also GloVe-style, no header -- confirmed directly against a real downloaded LMMS release) -- for users who only have the raw per-sensekey vectors, not a pre-built synset file.
Neither loader ingests SensEmBERT's own raw (pre-combination) vector
files -- SensEmBERT was never open-sourced and its own site
(sensembert.org) is no longer reachable, so there is no real file or
confirmed format to build and verify a loader against. Use
load_synset_centroid_vectors with a pre-built SensEmBERT+LMMS file (as
the reference's own release provides) to get the paper's full best
configuration; build_synset_centroid_vectors_from_lmms alone only
covers the LMMS-only configuration (still a real, evaluated row in the
paper's own Table 2, just not its best one).
build_synset_centroid_vectors_from_lmms ¶
build_synset_centroid_vectors_from_lmms(
path: str | Path,
vocabulary: SenseVocabulary,
base: Tensor,
target_dim: int | None = None,
lexicon: str = "omw-en:1.4",
progress: bool = True,
) -> tuple[torch.Tensor, int]
Build a synset-centroid init tensor from LMMS's own raw sensekey vectors.
Reimplements the paper's own recorded pipeline (Section 4.2, "Output
Embeddings" -- not ported from the reference's CC-BY-NC-SA-licensed
bin/get_centroids.py/bin/reduce_dims.py, though independently
confirmed to agree with them): each raw sense vector is L2-normalized,
every sense sharing a synset (via sensekey_to_synset_id) is averaged
into that synset's centroid, then (if target_dim differs from the
raw dimensionality) reduced with sklearn.decomposition.TruncatedSVD
and L2-normalized again -- the paper states the final vectors are
unit-length, and SVD projection doesn't preserve norms, so the second
normalization is necessary to actually reach that state, not optional
polish.
This resolves every sensekey in path via sensekey_to_synset_id
(one wn lookup each) -- a real LMMS release has ~100K-200K
sensekeys, so this is a slow, one-time vocabulary-init pass, not
something to call per training step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
A GloVe-style plaintext file, no header, one line per
sensekey: |
required |
vocabulary
|
SenseVocabulary
|
The |
required |
base
|
Tensor
|
A |
required |
target_dim
|
int | None
|
Reduce to this many dimensions via truncated SVD if
given and different from the file's own raw dimensionality
(the paper reduces LMMS's raw 2048d to 512d). |
None
|
lexicon
|
str
|
Passed to |
'omw-en:1.4'
|
progress
|
bool
|
Show a |
True
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
int
|
|
Source code in src/linkingtk/algorithms/wsd/_ewiser_sense_embeddings.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
load_synset_centroid_vectors ¶
load_synset_centroid_vectors(
path: str | Path,
vocabulary: SenseVocabulary,
base: Tensor,
lexicon: str = "omw-en:1.4",
progress: bool = True,
) -> tuple[torch.Tensor, int]
Load a pre-built, offset-keyed synset-vector file into base, in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
A GloVe-style plaintext file, no header, one line per
synset: |
required |
vocabulary
|
SenseVocabulary
|
The |
required |
base
|
Tensor
|
A |
required |
lexicon
|
str
|
Passed to |
'omw-en:1.4'
|
progress
|
bool
|
Show a |
True
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
int
|
for chaining) and the number of |
tuple[Tensor, int]
|
found for. |
Source code in src/linkingtk/algorithms/wsd/_ewiser_sense_embeddings.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
_ewiser_text ¶
Sentence/word alignment helpers for EwiserEncoder.
EWISER classifies one vector per whitespace word (mean-pooled from BERT
subwords), unlike
GlossBertEncoder,
which never needs word-level positions. These helpers resolve a mention's
character span to a word index, subword-tokenize each whitespace word in
isolation (see wordpiece_tokenize_words's docstring for why this can't
just be a high-level tokenizer call), and pool subword hidden states back
up to word level.
mean_pool_subwords ¶
mean_pool_subwords(
hidden: Tensor,
word_ids_batch: list[list[int | None]],
num_words: list[int],
) -> list[torch.Tensor]
Mean-pool hidden ([batch, seq, H]) subword vectors back to one vector per word.
word_ids_batch[b][t] is the word index subtoken t of sequence b
belongs to (None for special/padding tokens), from the tokenizer's
own fast word_ids() alignment.
Source code in src/linkingtk/algorithms/wsd/_ewiser_text.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
mention_sentence_and_span ¶
mention_sentence_and_span(
entity: Entity,
) -> tuple[str, int, int]
The sentence text and mention character span to run EwiserEncoder against.
Uses context's (text, start, end) offsets when it's a
ContextWithSpan (see linkingtk.core.entity) (the shape
SemCorDataset/
UfsacDataset mentions carry --
every mention from the same sentence shares an identical text, which
is what lets EwiserEncoder.score batch by unique sentence instead of
per pair). Falls back to treating the mention's own label as a
standalone one-word "sentence" when context carries no span.
Source code in src/linkingtk/algorithms/wsd/_ewiser_text.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
whitespace_tokenize_with_offsets ¶
whitespace_tokenize_with_offsets(
text: str,
) -> list[tuple[str, int, int]]
Split text on whitespace, keeping each word's character span.
Source code in src/linkingtk/algorithms/wsd/_ewiser_text.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
word_index_for_span ¶
word_index_for_span(
tokens: list[tuple[str, int, int]], start: int, end: int
) -> int | None
The index of tokens containing character span [start, end), or None.
Prefers an exact containment match (tok_start <= start < tok_end);
falls back to the token with the greatest character overlap for a
span that doesn't land cleanly on a whitespace-token boundary.
Source code in src/linkingtk/algorithms/wsd/_ewiser_text.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
wordpiece_tokenize_words ¶
wordpiece_tokenize_words(
tokenizer: PreTrainedTokenizerBase, words: list[str]
) -> tuple[list[str], list[int | None]]
Subword-tokenize each whitespace words entry in isolation, returning
([CLS] + subwords + [SEP], word_ids) (word_ids[t] is the index into
words subtoken t came from, None for [CLS]/[SEP]).
Deliberately not tokenizer(words, is_split_into_words=True):
that high-level call still runs BERT's own punctuation-splitting
pre-tokenizer on each provided "word" before wordpiece, so a word
containing internal punctuation (e.g. UFSAC's own single-token
"Oct.") gets split into two independent pre-tokens ("Oct",
".") before wordpiece ever runs, producing a standalone "."
token -- not the "##." continuation-piece EWISER's own
reference produces by wordpiece-tokenizing the literal string
"Oct." directly, with no pre-splitting step at all. Confirmed
directly: this single-token divergence ("." vs "##." are
different vocabulary entries with unrelated embeddings) was traced,
layer by layer, as the exact source of a checkpoint-reproduction gap
against EWISER's own published numbers -- affects any UFSAC word with
internal punctuation (abbreviations like "Oct."/"Sept.", likely also
"U.S.", possessives, etc.), a small fraction of tokens but enough to
measurably shift results once fed through the checkpoint's own
BatchNorm (calibrated tightly enough to the reference's exact
activations that even one wrong subtoken's ripple through attention
measurably shifts other positions' representations).
Implements BERT's WordPiece algorithm directly (greedy longest-match
against the tokenizer's own vocab) rather than depending on any
tokenizer-internal wordpiece_tokenizer attribute -- not exposed by
every transformers version/backend.
Source code in src/linkingtk/algorithms/wsd/_ewiser_text.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
ESC¶
esc ¶
ESC Word Sense Disambiguation.
ESC (Barba, Pasini & Navigli, "ESC: Redesigning WSD with Extractive Sense
Comprehension", NAACL 2021, https://github.com/SapienzaNLP/esc) reframes
WSD as extractive span comprehension: a mention's context sentence (with
the target word bracketed by literal <classify>/</classify>
markers, see insert_classify_markers)
is paired with every candidate sense's gloss, concatenated into one
sequence, and a standard extractive-QA head
(transformers.AutoModelForQuestionAnswering) predicts which candidate's
gloss span answers the "question" -- unlike
GlossBertLinker's
one-forward-pass-per-(mention, candidate) cross-encoder, or
EwiserLinker's one-forward
-pass-per-sentence full-inventory classifier: ESC is one forward pass per
mention, jointly observing every one of that mention's candidates at
once.
Reimplemented from the paper and from understanding the reference's own
source (esc/esc_pl_module.py, esc/utils/definitions_tokenizer.py), not
ported from it -- the reference repo is CC-BY-NC-SA 4.0, incompatible with
redistributing literal code under this package's Apache-2.0 license (same
constraint already documented in
EwiserEncoder's module
docstring for the EWISER port). Confirmed directly against the paper's own
published SemCor checkpoint (its Lightning hyper_parameters/state_dict,
not just the paper text):
transformer_model: facebook/bart-large,squad_head: False,use_special_tokens: False,use_pmask: False-- the checkpoint uses the plainAutoModelForQuestionAnsweringpath, not any of the reference's custom XLNet/SQuAD-head/special-token variants. Itsstate_dictholds onlyqa_model.model.*(a stock BART encoder+decoder) andqa_model.qa_outputs.{weight,bias}(aLinear(hidden_size, 2)start/end head).transformers'BartForQuestionAnswering.forwardruns the full encoder-decoder stack (not encoder-only) --decoder_input_idsdefaults to a right-shift ofinput_idswhen omitted -- and applies the QA head to the decoder's last hidden state. A real architectural detail of this checkpoint, not an incidental implementation choice: simply callingAutoModelForQuestionAnswering.from_pretrained(...)withinput_ids/attention_mask(and, for training,start_positions/end_positions) reproduces it exactly, with.losscomputed internally as HF's own(start_loss + end_loss) / 2cross-entropy -- no hand-rolled loss needed (see EscTrainer).- Candidate scoring uses the reference's own
"probabilistic"prediction type (the README's own example command, and what its headline numbers are reported under):start_logprob[start_token] + end_logprob[end_token]for each candidate's resolved gloss span, both taken from alog_softmaxover the full (padded) sequence.
See EscEncoder and EscLinker.
EscEncoder ¶
EscEncoder(
model_name_or_path: str = "facebook/bart-large",
max_length: int = 1024,
forward_batch_size: int = 8,
)
Bases: Module
Extractive-QA cross-encoder scorer for WSD.
Unlike GlossBertEncoder
(one forward pass per (mention, candidate) pair), score() runs one
forward pass per mention, jointly observing every one of that
mention's candidates present in pairs -- see score's own docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name_or_path
|
str
|
A Hugging Face question-answering model id
(e.g. |
'facebook/bart-large'
|
max_length
|
int
|
Maximum joint-sequence token length (truncated). |
1024
|
forward_batch_size
|
int
|
Number of distinct mentions |
8
|
Source code in src/linkingtk/algorithms/wsd/esc.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | |
forward ¶
forward(
input_ids: Tensor,
attention_mask: Tensor,
start_positions: Tensor | None = None,
end_positions: Tensor | None = None,
) -> Any
Thin pass-through to the wrapped QA model -- used by
EscTrainer for its
.loss (HF's own (start_loss + end_loss) / 2 cross-entropy) and
by score() for its .start_logits/.end_logits.
Source code in src/linkingtk/algorithms/wsd/esc.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
from_checkpoint
classmethod
¶
from_checkpoint(
checkpoint_path: str | Path,
model_name_or_path: str = "facebook/bart-large",
max_length: int = 1024,
forward_batch_size: int = 8,
) -> EscEncoder
Load the paper's own published Lightning checkpoint (e.g. the SemCor checkpoint linked from the reference README).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checkpoint_path
|
str | Path
|
Local path to a |
required |
model_name_or_path
|
str
|
The backbone architecture to construct
before loading the checkpoint's weights -- must match what
the checkpoint was trained with (its own
|
'facebook/bart-large'
|
max_length
|
int
|
Forwarded to |
1024
|
forward_batch_size
|
int
|
Forwarded to |
8
|
Returns:
| Type | Description |
|---|---|
EscEncoder
|
An |
Source code in src/linkingtk/algorithms/wsd/esc.py
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
score ¶
score(pairs: list[tuple[Entity, Entity]]) -> torch.Tensor
Return a (len(pairs),) tensor: each pair's start_logprob +
end_logprob for its candidate's resolved gloss span.
Unlike a cross-encoder's score, this does not run one forward
pass per pair: it groups pairs by mention, builds one joint
sequence per mention (context sentence + every one of that
mention's candidates' glosses, concatenated -- see
build_joint_sequence), and runs the encoder once per mention
(batched forward_batch_size mentions at a time). A candidate
whose gloss span can't be resolved within the (possibly truncated)
joint sequence scores -inf (never spuriously wins a match) --
same convention EwiserEncoder.score uses for an unresolved
candidate.
Source code in src/linkingtk/algorithms/wsd/esc.py
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
EscLinker ¶
EscLinker(
model: EscEncoder, matching: Matcher = DEFAULT_MATCHER
)
Bases: BaseLinker
WSD linking scored by an EscEncoder.
encoder = EscEncoder.from_checkpoint("escher_semcor_best.ckpt")
linker = EscLinker(encoder)
results = linker.link(mentions, senses)
Or, for from-scratch/continued training, construct EscEncoder
directly and train it via
EscTrainer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
EscEncoder
|
The |
required |
matching
|
Matcher
|
Strategy used to resolve scored candidates into final links. Defaults to GreedyMatcher. |
DEFAULT_MATCHER
|
Source code in src/linkingtk/algorithms/wsd/esc.py
328 329 330 | |
score_candidates ¶
score_candidates(
dataset1: list[Entity],
dataset2: list[Entity] | EntitySource,
blocking: BlockingStrategy = DEFAULT_BLOCKING,
) -> dict[str, list[tuple[str, float]]]
Blocked candidates per source entity, scored but not yet matched.
Satisfies CandidateScorer
-- what link() itself builds internally, exposed so
LlmRerankerLinker
(#23) can re-rank a narrowed top-k instead of every blocked pair.
Source code in src/linkingtk/algorithms/wsd/esc.py
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | |
_esc_text ¶
Joint-sequence construction helpers for EscEncoder.
ESC scores a mention's candidate senses with a single extractive-QA forward
pass per mention: the context sentence (with the target word bracketed by
literal <classify>/</classify> markers) followed by every
candidate's gloss, concatenated, tokenized as one (sequence1,
sequence2) pair. The correct candidate is identified by a token span
within sequence2 -- these helpers build that joint sequence and resolve
each candidate's span in it.
Reimplemented from understanding the reference's own
esc/utils/definitions_tokenizer.py (prepare_sample_without_st), not
copied -- the reference repo is CC-BY-NC-SA 4.0, incompatible with
redistributing literal code under this package's Apache-2.0 license (see
EwiserEncoder's module
docstring for the same constraint on the EWISER port). Uses the fast
tokenizer's own Encoding.sequence_ids property to tell sequence1's
tokens from sequence2's, rather than the reference's own heuristic of
scanning for two consecutive (0, 0)-offset tokens in a row -- a more
direct, less fragile way to find the same boundary.
build_joint_sequence ¶
build_joint_sequence(
tokenizer: PreTrainedTokenizerBase,
context_sentence: str,
candidate_glosses: list[str],
max_length: int,
) -> tuple[
torch.Tensor, torch.Tensor, list[tuple[int, int] | None]
]
Tokenize (context_sentence, " ".join(candidate_glosses)) as one
pair sequence and resolve each candidate's (start_token, end_token)
span within it.
Returns (input_ids, attention_mask, spans) for one sequence
(batching/padding happens at the caller, matching
EwiserEncoder._encode_chunk's existing pattern) -- input_ids/
attention_mask are 1-D. spans[i] is None if candidate i's gloss
span couldn't be resolved to token boundaries, or falls outside a
sequence truncated to max_length (the caller treats this the same
way EwiserEncoder.score treats an unresolved candidate: unscoreable,
not a crash).
Source code in src/linkingtk/algorithms/wsd/_esc_text.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
candidate_gloss ¶
candidate_gloss(sense: Entity) -> str
Definition text, capitalized, with a trailing period -- matches
the reference's own gloss formatting (synset.definition().capitalize()
+ ".").
Source code in src/linkingtk/algorithms/wsd/_esc_text.py
38 39 40 41 42 43 44 45 46 | |
insert_classify_markers ¶
insert_classify_markers(
text: str, start: int, end: int
) -> str
Bracket text's [start, end) mention span with literal
<classify>/</classify> markers (plain text, not added special
tokens -- matches the published checkpoint's own
use_special_tokens=False training configuration).
Source code in src/linkingtk/algorithms/wsd/_esc_text.py
49 50 51 52 53 54 55 | |
pad_encoded_sequences ¶
pad_encoded_sequences(
sequences: list[tuple[Tensor, Tensor]],
pad_token_id: int,
) -> tuple[torch.Tensor, torch.Tensor]
Right-pad a list of (input_ids, attention_mask) 1-D tensor pairs
(e.g. from repeated build_joint_sequence calls) into one batch.
Shared by EscEncoder's scoring path and EscTrainer's training path, so both batch mentions of differing joint-sequence length the same way.
Source code in src/linkingtk/algorithms/wsd/_esc_text.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
Word Sense Alignment¶
wsa ¶
Word Sense Alignment (WSA) algorithms.
WSA finds equivalent senses between two dictionary entries, either monolingually or across languages. Related systems: NAISC.