File size: 8,579 Bytes
f28d994
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
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
"""Generate post95 submissions with content-mean and BPR-MF score features."""

from __future__ import annotations

import argparse
import importlib.util
from pathlib import Path

import lightgbm as lgb
import numpy as np
import pandas as pd
import torch
import torch.nn.functional as F


def load_module(name: str, path: Path):
    spec = importlib.util.spec_from_file_location(name, path)
    module = importlib.util.module_from_spec(spec)
    assert spec.loader is not None
    spec.loader.exec_module(module)
    return module


def train_mf_test_scores(extra, root: Path, train_refs: pd.DataFrame, test_pairs: np.ndarray, out_dir: Path, device: str, seed: int, dim: int, epochs: int) -> np.ndarray:
    out_path = out_dir / f"test_mf_bpr_dynamic_s{seed}_d{dim}_e{epochs}.npy"
    if out_path.exists():
        return np.load(out_path)
    torch.manual_seed(seed)
    np.random.seed(seed)
    rng = np.random.default_rng(seed)
    train = train_refs[["source", "target"]].to_numpy(np.int64)
    train_set = set(map(tuple, train.tolist()))
    model = extra.MF(6611, 79937, dim).to(torch.device(device))
    opt = torch.optim.AdamW(model.parameters(), lr=0.01, weight_decay=1e-6)
    train_t = torch.as_tensor(train, dtype=torch.long, device=device)
    batch_size = 65536
    for ep in range(epochs):
        idx = torch.randint(0, train_t.size(0), (batch_size,), device=device)
        pos = train_t[idx]
        neg_np = np.empty((batch_size, 2), dtype=np.int64)
        authors = pos[:, 0].detach().cpu().numpy()
        filled = 0
        while filled < batch_size:
            papers = rng.integers(0, 79937, size=batch_size - filled)
            for a, p in zip(authors[filled:], papers):
                if (int(a), int(p)) not in train_set:
                    neg_np[filled] = (a, p)
                    filled += 1
                    if filled >= batch_size:
                        break
        neg = torch.as_tensor(neg_np, dtype=torch.long, device=device)
        loss = -F.logsigmoid(model.score(pos) - model.score(neg)).mean()
        opt.zero_grad()
        loss.backward()
        opt.step()
        if (ep + 1) % 20 == 0:
            print(f"mf-test epoch={ep+1:03d} loss={loss.item():.4f}")
    test_t = torch.as_tensor(test_pairs, dtype=torch.long, device=device)
    scores = []
    with torch.no_grad():
        for st in range(0, len(test_pairs), 131072):
            scores.append(model.score(test_t[st : st + 131072]).detach().cpu().numpy())
    scores = np.concatenate(scores).astype(np.float32)
    np.save(out_path, scores)
    return scores


def make_subs(root: Path, out_dir: Path, score: np.ndarray, ratios: list[float]) -> None:
    known = np.load(root / "cached_scores" / "test_known_mask.npy").astype(bool)
    for ratio in ratios:
        pred = np.zeros(len(score), dtype=np.int8)
        pred[np.argsort(score)[-int(round(len(score) * ratio)):]] = 1
        pred[known] = 1
        path = out_dir / f"submission_post95_content_mf_lgb_r{ratio:.3f}.csv"
        pd.DataFrame({"Index": np.arange(len(pred), dtype=np.int64), "Predicted": pred}).to_csv(path, index=False)
        print(path, int(pred.sum()), float(pred.mean()))
    pred = (score >= 0.5).astype(np.int8)
    pred[known] = 1
    path = out_dir / "submission_post95_content_mf_lgb_score_ge0.500.csv"
    pd.DataFrame({"Index": np.arange(len(pred), dtype=np.int64), "Predicted": pred}).to_csv(path, index=False)
    print(path, int(pred.sum()), float(pred.mean()))


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--package-root", type=Path, default=Path(__file__).resolve().parents[1])
    parser.add_argument("--split-seed", type=int, default=202)
    parser.add_argument("--main-val-score-file", type=Path, required=True)
    parser.add_argument("--device", default="cuda:0" if torch.cuda.is_available() else "cpu")
    parser.add_argument("--seed", type=int, default=202)
    parser.add_argument("--mf-dim", type=int, default=256)
    parser.add_argument("--mf-epochs", type=int, default=220)
    parser.add_argument("--ratios", nargs="*", type=float, default=[0.498, 0.500, 0.502, 0.504, 0.505])
    args = parser.parse_args()

    root = args.package_root
    stack = load_module("stack", root / "code" / "stack_rank_calibration.py")
    lgcn = load_module("lgcn", root / "code" / "train_val_lgcn_ensemble.py")
    post = load_module("post", root / "code" / "post95_ablation.py")
    gen = load_module("gen", root / "code" / "generate_post95_submission.py")
    extra = load_module("extra", root / "code" / "extra_score_sources_ablation.py")

    out_dir = root / "validation_runs" / f"dynamic_seed{args.split_seed}" / "extra_bprmf_submission"
    out_dir.mkdir(parents=True, exist_ok=True)

    train_refs, val_pairs = lgcn.make_notebook_style_split(root, args.split_seed, 0.9)
    val_pairs_arr = val_pairs[["source", "target"]].to_numpy(np.int64)
    y = val_pairs["label"].to_numpy(np.int8)
    main_val = np.load(args.main_val_score_file).astype(np.float32)

    val_builder = stack.ExplicitGraphFeatures(root, train_refs)
    Xh = val_builder.transform(val_pairs_arr)
    X_val = np.column_stack(
        [
            stack.add_rank_features(val_pairs_arr, main_val),
            Xh,
            post.negative_evidence_features(Xh, main_val),
            gen.topk_content_similarity_fast(root, val_pairs_arr, val_builder),
        ]
    ).astype(np.float32)
    selected = [Path(x.strip()) for x in (root / "validation_runs" / f"dynamic_seed{args.split_seed}" / "post95_submission" / "selected_variant_val_scores.txt").read_text().splitlines() if x.strip()]
    X_val = np.column_stack([X_val, gen.variant_feature_matrix(post, [np.load(p).astype(np.float32) for p in selected])]).astype(np.float32)
    content_val = extra.content_mean_score(root, val_pairs_arr, val_builder)
    mf_val = np.load(root / "validation_runs" / f"dynamic_seed{args.split_seed}" / "extra_score_sources" / f"val_mf_bpr_s{args.seed}_d{args.mf_dim}.npy").astype(np.float32)
    Xc, _ = extra.score_to_features(content_val, "content_mean_cos", val_pairs_arr)
    Xm, _ = extra.score_to_features(mf_val, "mf_bpr", val_pairs_arr)
    X_val = np.column_stack([X_val, Xc, Xm]).astype(np.float32)
    print("fit LightGBM", X_val.shape)
    clf = lgb.LGBMClassifier(
        n_estimators=1200,
        learning_rate=0.025,
        num_leaves=31,
        subsample=0.9,
        colsample_bytree=0.9,
        reg_lambda=5.0,
        min_child_samples=80,
        objective="binary",
        verbose=-1,
        random_state=args.seed,
    )
    clf.fit(X_val, y)

    test_pairs = np.array(gen.read_txt(root / "data_and_docs" / "bipartite_test_ann.txt"), dtype=np.int64)
    main_test = np.load(root / "validation_runs" / f"dynamic_seed{args.split_seed}" / "post95_test_scores" / "dyn202_l2d512_bpr_bigbatch_more" / "scores" / "test_vanilla_ensemble_mean.npy").astype(np.float32)
    full_refs = pd.DataFrame(gen.read_txt(root / "data_and_docs" / "bipartite_train_ann.txt"), columns=["source", "target"])
    test_builder = stack.ExplicitGraphFeatures(root, full_refs)
    Xht = test_builder.transform(test_pairs)
    X_test = np.column_stack(
        [
            stack.add_rank_features(test_pairs, main_test),
            Xht,
            post.negative_evidence_features(Xht, main_test),
            gen.topk_content_similarity_fast(root, test_pairs, test_builder),
        ]
    ).astype(np.float32)
    test_scores = []
    for p in selected:
        rel = p.resolve().relative_to(root / "validation_runs" / f"dynamic_seed{args.split_seed}")
        tp = root / "validation_runs" / f"dynamic_seed{args.split_seed}" / "post95_test_scores" / rel.parent / rel.name.replace("val_", "test_", 1)
        test_scores.append(np.load(tp).astype(np.float32))
    X_test = np.column_stack([X_test, gen.variant_feature_matrix(post, test_scores)]).astype(np.float32)
    content_test = extra.content_mean_score(root, test_pairs, test_builder)
    mf_test = train_mf_test_scores(extra, root, train_refs, test_pairs, out_dir, args.device, args.seed, args.mf_dim, args.mf_epochs)
    Xct, _ = extra.score_to_features(content_test, "content_mean_cos", test_pairs)
    Xmt, _ = extra.score_to_features(mf_test, "mf_bpr", test_pairs)
    X_test = np.column_stack([X_test, Xct, Xmt]).astype(np.float32)
    print("predict", X_test.shape)
    pred_score = clf.predict_proba(X_test)[:, 1].astype(np.float32)
    np.save(out_dir / "test_post95_content_mf_lgb_pred.npy", pred_score)
    make_subs(root, out_dir, pred_score, args.ratios)


if __name__ == "__main__":
    main()