kg_emb models: default L3 penalty has no coefficient, negative scores are summed, and ns other than "adv" breaks TransE and RotatE
メンテナーはふだん 3 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 35/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
調査の方向性
Start with pyhealth/medcode/pretrained_embeddings/kg_emb/models/kg_base.py, then read distmult.py, complex.py, transe.py, and rotate.py alongside the linked RotatE reference implementation. Check the reported regularization, negative scoring, and non-adv sampling behaviors, and add the hand-set embedding tests the issue proposes. Done means those tests cover the reported defects and the models train and score consistently across supported options.
索引モデルが issue の本文から書いたものです。
説明
These are pre-existing defects in pyhealth/medcode/pretrained_embeddings/kg_emb/models. On master the code is unreachable, since kg_emb fails to import (#952); #1202 makes the module importable again and leaves the behaviour described here unchanged. In the rejection loops it replaces the deprecated np.in1d(..., assume_unique=True) with np.isin(...) without that flag: the draws are made with replacement, and on NumPy's sort path the flag dropped repeated draws of a valid negative, so the negatives drawn on larger graphs change, but the non-termination described below does not. Fixing these defects changes trained models and reported metrics, so, as @jhnwu3 agreed on Discord on 27 Oct, they are tracked here rather than in that PR. Links point to master at 0bfc345.
1. The default L3 penalty of DistMult and ComplEx has no coefficient
DistMult and ComplEx default to use_regularization="l3" (distmult.py#L21, complex.py#L22). KGEBaseModel.forward adds l3_regularization() to the loss with no weight (kg_base.py#L307-L308), and that term sums |E|^3 + |R|^3 over the whole entity and relation tables (distmult.py#L33-L34, complex.py#L44-L45), not over the embeddings of the batch.
The penalty then dominates the loss. On a synthetic graph of 200 entities in 10 clusters of 20, with one relation ("same cluster", 2,000 of the 3,800 ordered within-cluster pairs, split into 1,600 training and 400 test triples), trained for 60 epochs with Adam (lr 1e-3, batch 64) and otherwise default model arguments, on the #1202 branch: the mean training loss approaches ln 2 = 0.6931 and stays there (0.6999 for DistMult and 0.6932 for ComplEx at epoch 10, 0.6931 at epoch 60), every entity embedding entry shrinks below 1e-3 in absolute value, and the filtered test MRR, over head and tail prediction, stays at the level of an untrained model:
| Model | MRR, init seed 0 | MRR, init seed 1 |
|---|---|---|
| DistMult, untrained | 0.031 | 0.024 |
| DistMult, default L3 | 0.024 | 0.029 |
DistMult, use_regularization=None |
0.319 | 0.304 |
| ComplEx, untrained | 0.023 | 0.030 |
| ComplEx, default L3 | 0.027 | 0.032 |
ComplEx, use_regularization=None |
0.269 | 0.265 |
Script (runs on the #1202 branch; the MRR is PyHealth's own filtered evaluation)
"""Filtered test MRR of DistMult and ComplEx with and without their default
L3 penalty, on a synthetic clustered graph.
Runs on the branch of sunlabuiuc/PyHealth#1202, where kg_emb imports.
Consumes nothing on disk; prints one line per run.
The graph has 200 entities in 10 clusters of 20 and one relation, "same
cluster": 2,000 of the 3,800 ordered pairs of distinct entities within a
cluster, split 80/20. The models keep their default arguments apart from
use_regularization. The filtered MRR is PyHealth's: forward in eval mode,
which filters every known train or test triple, then multiclass_metrics_fn,
over head and tail prediction.
"""
import numpy as np
import polars as pl
import torch
from pyhealth.medcode.pretrained_embeddings.kg_emb.datasets import KnownTriples
from pyhealth.medcode.pretrained_embeddings.kg_emb.models import ComplEx, DistMult
from pyhealth.metrics import multiclass_metrics_fn
from pyhealth.processors import KGTripleProcessor
N_ENTITIES, N_CLUSTERS, N_TRIPLES = 200, 10, 2000
TRAIN_FRACTION = 0.8
EPOCHS, BATCH, LR = 60, 64, 1e-3
GRAPH_SEED = 0
INIT_SEEDS = (0, 1)
def make_graph():
size = N_ENTITIES // N_CLUSTERS
pairs = [
(c * size + i, 0, c * size + j)
for c in range(N_CLUSTERS)
for i in range(size)
for j in range(size)
if i != j
]
order = np.random.default_rng(GRAPH_SEED).permutation(len(pairs))[:N_TRIPLES]
triples = [pairs[k] for k in order]
n_train = int(TRAIN_FRACTION * len(triples))
return triples[:n_train], triples[n_train:]
class Part:
"""Stands in for a set_task part: a processor fitted on the training
triples, and the known triples of both parts."""
def __init__(self, train, test):
processor = KGTripleProcessor(num_entities=N_ENTITIES, num_relations=1)
processor.fit([{"triple": t} for t in train], "triple")
self.input_processors = {"triple": processor}
ids = pl.DataFrame(
train + test, schema=["head_id", "relation_id", "tail_id"], orient="row"
)
self.known_triples = KnownTriples.from_ids(ids)
def filtered_mrr(model, test):
model.eval()
with torch.no_grad():
out = model(triple=torch.tensor(test))
scores = multiclass_metrics_fn(
out["y_true"].numpy(), out["y_prob"].numpy(), metrics=["mean_rank"]
)
return scores["mean_reciprocal_rank"]
def build(cls, part, seed, regularization):
torch.manual_seed(seed)
np.random.seed(seed) # training negatives are drawn with np.random
return cls(part, use_regularization=regularization)
def train(model, triples):
optimizer = torch.optim.Adam(model.parameters(), lr=LR)
x = torch.tensor(triples)
model.train()
epoch_means = []
for _ in range(EPOCHS):
order = torch.randperm(len(x))
losses = []
for start in range(0, len(x), BATCH):
loss = model(triple=x[order[start:start + BATCH]])["loss"]
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses.append(loss.item())
epoch_means.append(float(np.mean(losses)))
return epoch_means
if __name__ == "__main__":
train_triples, test_triples = make_graph()
part = Part(train_triples, test_triples)
print(f"{len(train_triples)} train / {len(test_triples)} test triples")
for seed in INIT_SEEDS:
for cls in (DistMult, ComplEx):
untrained = build(cls, part, seed, "l3")
print(f"seed {seed} {cls.__name__} untrained: MRR {filtered_mrr(untrained, test_triples):.3f}")
for regularization in ("l3", None):
model = build(cls, part, seed, regularization)
means = train(model, train_triples)
print(
f"seed {seed} {cls.__name__} use_regularization={regularization}: "
f"MRR {filtered_mrr(model, test_triples):.3f}, mean loss at epochs "
f"10/20/60 {means[9]:.4f}/{means[19]:.4f}/{means[-1]:.4f}, "
f"max |E| {model.E_emb.detach().abs().max():.1e}",
flush=True,
)
The RotatE reference implementation (DeepGraphLearning/KnowledgeGraphEmbedding) multiplies its regularization term by an argument that defaults to 0. Two related points in the same lines: the KGEBaseModel docstring says regularization is "False by default" (kg_base.py#L22), and any value other than None or "l3" calls self.regularization() with no argument (kg_base.py#L309-L310), which raises TypeError at the first training step, since every model's regularization takes a batch. #1202 corrects the docstring, not this behaviour.
A possible fix: a named coefficient, 0 or small by default; the penalty computed on the embeddings of the batch, as in the N3 regularizer of Lacroix et al. (2018); and a ValueError in __init__ for unsupported values.
2. Head and tail negative scores are added before the loss
In training, forward adds the i-th head-corrupted score to the i-th tail-corrupted score (kg_base.py#L288), then applies the self-adversarial weighting or logsigmoid to that sum (kg_base.py#L290-L294). The loss therefore depends on s(h'_i, r, t) + s(h, r, t'_i), which is the score of no triple, and a hard negative paired with an easy one gets almost no gradient. The RotatE reference scores each negative on its own, alternating head-corrupted and tail-corrupted batches. Concatenating the two score tensors along the negative dimension before the weighting would give a per-negative loss.
3. ns != "adv" is broken for TransE and RotatE
TransE.calcreturns+||h + r - t||unlessns == "adv"(transe.py#L44-L47), whereas the loss, the ranking metrics andinference()treat a higher score as more plausible. Training withns="uniform"orns="normal"therefore optimizes the opposite of the TransE objective.RotatE.calcreadsself.r_emb_range(rotate.py#L40), whichKGEBaseModel.__init__defines only whenns == "adv"(kg_base.py#L65-L70), so any other value raisesAttributeErrorat the first forward pass.
The four models default to ns="adv", which is not affected. A possible fix: return -distance, or margin - distance, for every ns in TransE; in RotatE, compute the phase scale independently of ns, or reject other values in __init__.
Smaller points in the same code
- The rejection loops that draw training negatives (kg_base.py#L153, #L169) never end when a pair's filter set covers every entity, which only a degenerate graph produces.
- The
lossreturned in evaluation (kg_base.py#L331-L332) treats every candidate as a negative, the true entity included, so it should not serve as a validation criterion.
I can open a PR for these once #1202 is merged, with tests on hand-set embeddings for each.
- 主要言語
- Python
- スター
- 1.7k
- フォーク
- 803
- 平均マージ
- 5日 7時間
- マージ済み PR(30日)
- 58
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
sunlabuiuc/PyHealth のほかの issue
-
難易度 4/5 3〜5日 初心者へのやさしさ 30/100
sunlabuiuc/PyHealth#1297 ·
メンテナーはふだん 3 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 22/100
sunlabuiuc/PyHealth#1295 · コメント 1 件 ·
メンテナーはふだん 3 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 42/100
sunlabuiuc/PyHealth#1294 · コメント 1 件 ·
メンテナーはふだん 3 日以内に返信
-
Internal transfers are counted as readmissions対応中かも @thebean27 が 23 日前に担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
sunlabuiuc/PyHealth#1269 · コメント 3 件 ·
メンテナーはふだん 3 日以内に返信
-
PyHealth 2.0: several modules still target the 1.x dataset API対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
sunlabuiuc/PyHealth#1201 · コメント 4 件 ·
メンテナーはふだん 3 日以内に返信
sunlabuiuc/PyHealth の issue をすべて見る
似ている issue
-
Support .aac audio uploads対応中かも @julien731 が今日担当しました。 オープンfeature
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
nimblehq/audio-transcriber#153 ·
-
help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
antonyrag/ragleap-core#1062 ·
-
needs-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
gke-labs/kube-agents#4321 ·
メンテナーはふだん 1 日以内に返信
-
enhancement help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
Git-Hospital/githealthcheck#61 ·
メンテナーはふだん 1 日以内に返信
-
changelog investigate
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
ramnes/notion-sdk-py#433 ·