Jyo-K commited on
Commit
fc0d2a6
·
verified ·
1 Parent(s): c43c544

Upload folder using huggingface_hub (part 5)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +9 -0
  2. model-4/Colab/src/text_utils.py +264 -0
  3. model-4/Colab/utils/validate_submission.py +349 -0
  4. model-4/DESIGN.md +220 -0
  5. model-4/Documentation_template.md +129 -0
  6. model-4/IMPLEMENTATION.md +144 -0
  7. model-4/README.md +219 -0
  8. model-4/hf_cache/.agent_harnesses.json +1 -0
  9. model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3.lock +0 -0
  10. model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/27b905d788d20826414d27ce9fa9765623e38ea7.lock +0 -0
  11. model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959.lock +0 -0
  12. model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead.lock +0 -0
  13. model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/acfd94e399c5659e4bed75f91b4ee24b111fc7a6.lock +0 -0
  14. model-4/hf_cache/hub/CACHEDIR.TAG +4 -0
  15. model-4/hf_cache/hub/blobs/.huggingface-shared-blobs +1 -0
  16. model-4/hf_cache/hub/blobs/7c/7ccdaee489cbb709d4624beb0f0556fc5552e6759bde1d881fca5c7a9ee5462c +3 -0
  17. model-4/hf_cache/hub/blobs/7c/7ccdaee489cbb709d4624beb0f0556fc5552e6759bde1d881fca5c7a9ee5462c.lock +0 -0
  18. model-4/hf_cache/hub/blobs/7c/7ccdaee489cbb709d4624beb0f0556fc5552e6759bde1d881fca5c7a9ee5462c.refs +1 -0
  19. model-4/hf_cache/hub/blobs/99/994dbaac4eaaaf1033ce39ecedd612e77b89dbfe8efd8de25ca24bea40caf6bc +3 -0
  20. model-4/hf_cache/hub/blobs/99/994dbaac4eaaaf1033ce39ecedd612e77b89dbfe8efd8de25ca24bea40caf6bc.lock +0 -0
  21. model-4/hf_cache/hub/blobs/99/994dbaac4eaaaf1033ce39ecedd612e77b89dbfe8efd8de25ca24bea40caf6bc.refs +1 -0
  22. model-4/hf_cache/hub/blobs/c4/c4b914ec5ab295fbee3b0e0fe62f22febb3c09d34dd3778745e0de034a76df33 +3 -0
  23. model-4/hf_cache/hub/blobs/c4/c4b914ec5ab295fbee3b0e0fe62f22febb3c09d34dd3778745e0de034a76df33.lock +0 -0
  24. model-4/hf_cache/hub/blobs/c4/c4b914ec5ab295fbee3b0e0fe62f22febb3c09d34dd3778745e0de034a76df33.refs +1 -0
  25. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/added_tokens.json +0 -0
  26. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/chat_template.jinja +0 -0
  27. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/model.safetensors +0 -0
  28. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/model.safetensors.index.json +0 -0
  29. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/special_tokens_map.json +0 -0
  30. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/tokenizer.json +0 -0
  31. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3 +3 -0
  32. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/27b905d788d20826414d27ce9fa9765623e38ea7 +22 -0
  33. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959 +3 -0
  34. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead +3 -0
  35. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/acfd94e399c5659e4bed75f91b4ee24b111fc7a6 +4 -0
  36. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/refs/main +1 -0
  37. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/refs/refs/pr/5 +1 -0
  38. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/config.json +22 -0
  39. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/pytorch_model.bin +3 -0
  40. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/spm.model +3 -0
  41. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/tokenizer_config.json +4 -0
  42. model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/faf660b811036d7b6dfc53aacfe5281447d4e2c6/model.safetensors +3 -0
  43. model-4/output/candidate_pairs.tsv +3 -0
  44. model-4/output/matching_results.tsv +3 -0
  45. model-4/output_smoke/candidate_pairs.tsv +0 -0
  46. model-4/output_smoke/matching_results.tsv +0 -0
  47. model-4/requirements.txt +16 -0
  48. model-4/scripts/run.sh +21 -0
  49. model-4/src/blocking.py +105 -0
  50. model-4/src/ce_infer_help.py +38 -0
.gitattributes CHANGED
@@ -48,3 +48,12 @@ model-3/work/results_A/model_bundle.joblib.bak_ef05 filter=lfs diff=lfs merge=lf
48
  model-4/.venv/bin/python filter=lfs diff=lfs merge=lfs -text
49
  model-4/.venv/bin/python3 filter=lfs diff=lfs merge=lfs -text
50
  model-4/.venv/bin/python3.12 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
48
  model-4/.venv/bin/python filter=lfs diff=lfs merge=lfs -text
49
  model-4/.venv/bin/python3 filter=lfs diff=lfs merge=lfs -text
50
  model-4/.venv/bin/python3.12 filter=lfs diff=lfs merge=lfs -text
51
+ model-4/hf_cache/hub/blobs/7c/7ccdaee489cbb709d4624beb0f0556fc5552e6759bde1d881fca5c7a9ee5462c filter=lfs diff=lfs merge=lfs -text
52
+ model-4/hf_cache/hub/blobs/99/994dbaac4eaaaf1033ce39ecedd612e77b89dbfe8efd8de25ca24bea40caf6bc filter=lfs diff=lfs merge=lfs -text
53
+ model-4/hf_cache/hub/blobs/c4/c4b914ec5ab295fbee3b0e0fe62f22febb3c09d34dd3778745e0de034a76df33 filter=lfs diff=lfs merge=lfs -text
54
+ model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3 filter=lfs diff=lfs merge=lfs -text
55
+ model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959 filter=lfs diff=lfs merge=lfs -text
56
+ model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead filter=lfs diff=lfs merge=lfs -text
57
+ model-4/output/candidate_pairs.tsv filter=lfs diff=lfs merge=lfs -text
58
+ model-4/output/matching_results.tsv filter=lfs diff=lfs merge=lfs -text
59
+ model-4/work/ce/final/tokenizer.json filter=lfs diff=lfs merge=lfs -text
model-4/Colab/src/text_utils.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Country-agnostic normalisation. No hand-written dictionaries: legal forms are discovered per country
2
+ from unlabeled names, abbreviations are matched by a generic prefix/subsequence rule."""
3
+ import math
4
+ import re
5
+ from collections import Counter
6
+ from itertools import combinations
7
+
8
+ from anyascii import anyascii
9
+ from rapidfuzz.distance import JaroWinkler
10
+
11
+ _PRE = str.maketrans({"°": "o ", "º": "o ","’": "'", "`": "'", " ": " "})
12
+ _DOTTED = re.compile(r"\b(?:[a-z]\.){2,}")
13
+ _NONALNUM = re.compile(r"[^a-z0-9]+")
14
+ _DOMAIN = re.compile(r"^(?:www\.)?([a-z0-9][a-z0-9\-]*)\.[a-z]{2,4}$")
15
+ _JOIN_ALNUM = re.compile(r"(?<=[a-z0-9])[-/](?=[a-z0-9])")
16
+ _HOUSE_SEG = re.compile(r"[^a-z0-9#\-/. ]")
17
+ _HOUSE = re.compile(r"^(?:#|[a-z]{1,3}\.?)?\s*(\d{1,6})(?:\s*-\s*(\d{1,6})\b|([a-z])\b|\s*/\s*([a-z0-9]{1,3})\b)?")
18
+ _DIGITS = re.compile(r"\d+")
19
+ _LETTER = re.compile(r"[a-z]")
20
+
21
+
22
+ def is_nonlatin(s):
23
+ return any(ord(ch) > 0x24F for ch in s) if s else False
24
+
25
+
26
+ def ascii_lower(s):
27
+ if not s:
28
+ return ""
29
+ return anyascii(s.translate(_PRE)).lower()
30
+
31
+
32
+ def norm(s_ascii_lower):
33
+ s = _DOTTED.sub(lambda m: m.group(0).replace(".", ""), s_ascii_lower)
34
+ return _NONALNUM.sub(" ", s).strip()
35
+
36
+
37
+ # ----------------------------------------------------------------------------------------------- names
38
+
39
+ def name_tokens(raw):
40
+ a = ascii_lower(raw).strip()
41
+ if a and " " not in a:
42
+ m = _DOMAIN.match(a)
43
+ if m:
44
+ a = m.group(1)
45
+ return norm(a).split()
46
+
47
+
48
+ def discover_legal_forms(token_lists, min_frac, boundary_ratio, max_cooc, bigram_tight):
49
+ """Legal forms = spans that sit at a name boundary, are frequent, and are mutually exclusive with each
50
+ other (SARL/SAS/EURL never co-occur, while descriptors like 'groupe' co-occur with a legal form)."""
51
+ n = len(token_lists)
52
+ bcount, total_uni = Counter(), Counter()
53
+ for t in token_lists:
54
+ total_uni.update(set(t))
55
+ if len(t) < 2:
56
+ continue
57
+ spans = {(t[-1],), (t[0],)}
58
+ if len(t) > 2:
59
+ spans |= {tuple(t[-2:]), tuple(t[:2])}
60
+ bcount.update(spans)
61
+ cands = [s for s, c in bcount.items() if c >= min_frac * n]
62
+ cset = set(cands)
63
+ total, occs, nbr = Counter(), [], {}
64
+ for t in token_lists:
65
+ if len(t) >= 2:
66
+ for k in (1, 2):
67
+ if len(t) > k:
68
+ for s, nb in ((tuple(t[-k:]), t[-k - 1]), (tuple(t[:k]), t[k])):
69
+ if s in cset:
70
+ nbr.setdefault(s, Counter())[nb] += 1
71
+ occ = []
72
+ for k in (1, 2):
73
+ for i in range(len(t) - k + 1):
74
+ s = tuple(t[i:i + k])
75
+ if s in cset:
76
+ occ.append((s, i, i + k))
77
+ if occ:
78
+ total.update({o[0] for o in occ})
79
+ occs.append(occ)
80
+ cands = [s for s in cands if bcount[s] >= boundary_ratio * total[s]]
81
+ # bigram must be "tight": its first token rarely appears outside it (private->private limited)
82
+ cands = [s for s in cands if len(s) == 1 or total[s] >= bigram_tight * total_uni[s[0]]]
83
+ # a legal form attaches to many different words; 'notre' is nearly always next to 'sainte'
84
+ in_bigram = {x for s in cands if len(s) == 2 for x in s}
85
+
86
+ def dominated(s):
87
+ c = nbr.get(s)
88
+ return bool(c) and s[0] not in in_bigram and c.most_common(1)[0][1] > 0.5 * sum(c.values())
89
+
90
+ cands = [s for s in cands if not dominated(s)]
91
+ keep = set(cands)
92
+ co = Counter()
93
+ for occ in occs:
94
+ occ = [o for o in occ if o[0] in keep]
95
+ for (a, ai, aj), (b, bi, bj) in combinations(occ, 2):
96
+ if a != b and (aj <= bi or bj <= ai):
97
+ co[frozenset((a, b))] += 1
98
+ accepted = []
99
+ for s in sorted(cands, key=lambda x: -bcount[x]):
100
+ if all(co[frozenset((s, a))] <= max_cooc * min(total[s], total[a]) for a in accepted):
101
+ accepted.append(s)
102
+ return sorted(accepted, key=lambda x: (-len(x), x))
103
+
104
+
105
+ def strip_legal(tokens, forms, long_single):
106
+ t, found = list(tokens), []
107
+ changed = True
108
+ while changed and len(t) > 1:
109
+ changed = False
110
+ for f in forms:
111
+ k = len(f)
112
+ if len(t) > k and tuple(t[-k:]) == f:
113
+ found.append(" ".join(f)); del t[-k:]; changed = True; break
114
+ if len(t) > k and tuple(t[:k]) == f:
115
+ found.append(" ".join(f)); del t[:k]; changed = True; break
116
+ if len(t) > 1:
117
+ t2 = [x for x in t if x not in long_single]
118
+ if t2 and len(t2) < len(t):
119
+ found += [x for x in t if x in long_single]
120
+ t = t2
121
+ return t, " ".join(sorted(found))
122
+
123
+
124
+ def process_name(raw, forms, long_single):
125
+ toks = name_tokens(raw)
126
+ core, legal = strip_legal(toks, forms, long_single)
127
+ return {"name_full": " ".join(toks), "name_core": " ".join(core), "legal": legal,
128
+ "name_ns": "".join(core), "nonlatin": is_nonlatin(raw)}
129
+
130
+
131
+ # ----------------------------------------------------------------------------------------------- addresses
132
+
133
+ def parse_address(raw):
134
+ raw = raw or ""
135
+ segs = [s for s in re.split(r"[,;]", raw) if s.strip()]
136
+ house, street_i, street_rest = "", -1, None
137
+ for i, seg in enumerate(segs):
138
+ s = _HOUSE_SEG.sub(" ", ascii_lower(seg)).strip()
139
+ m = _HOUSE.match(s)
140
+ if m and _LETTER.search(s[m.end():]):
141
+ base, hi, suf1, suf2 = m.group(1), m.group(2), m.group(3), m.group(4)
142
+ house = f"{int(base)}|{suf1 or suf2 or ''}|{int(hi) if hi else ''}"
143
+ street_i, street_rest = i, s[m.end():]
144
+ break
145
+ if street_i < 0 and segs:
146
+ street_i, street_rest = 0, ascii_lower(segs[0])
147
+ street = norm(street_rest or "")
148
+ loc = [norm(ascii_lower(s)) for j, s in enumerate(segs) if j != street_i]
149
+ loc = [x for x in loc if x]
150
+ al = ascii_lower(raw)
151
+ full = norm(al)
152
+ nums = _DIGITS.findall(full)
153
+ hb = house.split("|")[0] if house else None
154
+ postal = sorted({x for x in nums if len(x) in (5, 6) and x != hb})
155
+ alnum = sorted({t for t in norm(_JOIN_ALNUM.sub("", al)).split() if _LETTER.search(t) and any(c.isdigit() for c in t)})
156
+ return {"addr_norm": full, "street": street, "street_ns": street.replace(" ", ""), "loc": "|".join(loc),
157
+ "house": house, "nums": " ".join(sorted(set(nums))), "postal": " ".join(postal),
158
+ "alnum": " ".join(alnum), "addr_empty": not full}
159
+
160
+
161
+ def house_compare(ha, hb):
162
+ """-> (state, log abs diff). 0 both missing, 1 one missing, 2 equal, 3 base equal/suffix differs,
163
+ 4 range contains, 5 conflict."""
164
+ if not ha and not hb:
165
+ return 0, math.nan
166
+ if not ha or not hb:
167
+ return 1, math.nan
168
+ a, b = ha.split("|"), hb.split("|")
169
+ ab, bb = int(a[0]), int(b[0])
170
+ if ab == bb:
171
+ return (2 if a[1] == b[1] else 3), 0.0
172
+ if (a[2] and ab <= bb <= int(a[2])) or (b[2] and bb <= ab <= int(b[2])):
173
+ return 4, 0.0
174
+ return 5, math.log1p(abs(ab - bb))
175
+
176
+
177
+ def set_state(a, b):
178
+ """0 both missing, 1 one missing, 2 share, 3 both present disjoint same lengths, 4 disjoint + length differs."""
179
+ if not a and not b:
180
+ return 0
181
+ if not a or not b:
182
+ return 1
183
+ if a & b:
184
+ return 2
185
+ return 4 if {len(x) for x in a}.isdisjoint({len(x) for x in b}) else 3
186
+
187
+
188
+ # ----------------------------------------------------------------------------------------------- token matching
189
+
190
+ def is_subseq(short, long):
191
+ it = iter(long)
192
+ return all(c in it for c in short)
193
+
194
+
195
+ def abbrev_compat(a, b):
196
+ if a == b:
197
+ return True
198
+ s, l = (a, b) if len(a) <= len(b) else (b, a)
199
+ return len(s) <= 4 and len(l) > len(s) and s[0] == l[0] and not s.isdigit() and is_subseq(s, l)
200
+
201
+
202
+ def tok_sim(a, b):
203
+ if a == b:
204
+ return 1.0
205
+ if a.isdigit() or b.isdigit():
206
+ return 0.0
207
+ if abbrev_compat(a, b):
208
+ return 0.9
209
+ if len(a) >= 4 and len(b) >= 4:
210
+ s = JaroWinkler.similarity(a, b)
211
+ if s >= 0.9:
212
+ return 0.9 * s
213
+ return 0.0
214
+
215
+
216
+ def soft_cov(ta, tb):
217
+ """Fraction of ta covered by tb (exact / abbreviation / typo tolerant)."""
218
+ if not ta:
219
+ return math.nan
220
+ if not tb:
221
+ return 0.0
222
+ sb = set(tb)
223
+ tot = 0.0
224
+ for x in ta:
225
+ if x in sb:
226
+ tot += 1.0
227
+ continue
228
+ best = 0.0
229
+ for y in tb:
230
+ s = tok_sim(x, y)
231
+ if s > best:
232
+ best = s
233
+ tot += best
234
+ return tot / len(ta)
235
+
236
+
237
+ def contains_seq(a, b):
238
+ k = len(a)
239
+ if k == 0 or k > len(b):
240
+ return False
241
+ return any(b[i:i + k] == a for i in range(len(b) - k + 1))
242
+
243
+
244
+ def acronym_match(ta, tb):
245
+ if len(ta) >= 2:
246
+ ini = "".join(t[0] for t in ta)
247
+ if ini in tb or ini == "".join(tb):
248
+ return True
249
+ return False
250
+
251
+
252
+ def legal_state(la, lb):
253
+ if not la and not lb:
254
+ return 0
255
+ if not la or not lb:
256
+ return 1
257
+ if la == lb:
258
+ return 2
259
+ ta, tb = la.split(), lb.split()
260
+ if len(ta) == len(tb) and all(abbrev_compat(x, y) for x, y in zip(ta, tb)):
261
+ return 3
262
+ if abbrev_compat(la.replace(" ", ""), lb.replace(" ", "")):
263
+ return 3
264
+ return 4
model-4/Colab/utils/validate_submission.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ML Challenge 2026 — Submission Validator
4
+
5
+ Run this BEFORE submitting. It checks your output files against every formatting
6
+ rule the scorer enforces, so you can catch a rejection locally instead of burning
7
+ a submission. It reads only your output files and the test source files (to learn
8
+ which S1 entities are required and which S2/S3 IDs exist); it never needs the
9
+ ground truth and never computes your score.
10
+
11
+ It validates two files:
12
+
13
+ * ``matching_results.tsv`` (required) — your final matches, the file scored on the
14
+ leaderboard.
15
+ * ``candidate_pairs.tsv`` (optional) — the candidate set from your blocking stage.
16
+ When present, the validator also checks that your final matches are a subset of
17
+ your candidates and *warns* (never fails) otherwise. When absent it is skipped
18
+ with a warning; it is still expected in your final submission zip.
19
+
20
+ Stdlib only, Python 3.8+. Run from the ``student_resource/`` directory::
21
+
22
+ python3 utils/validate_submission.py \
23
+ --matching output/matching_results.tsv \
24
+ --candidate output/candidate_pairs.tsv \
25
+ --test-dir dataset/test
26
+
27
+ Exit code 0 means the files are safe to submit; 1 means fix the listed issues
28
+ (warnings never fail the run).
29
+
30
+ ID-existence check (off by default). By default the validator does NOT check that
31
+ every matched/candidate ID actually exists in the test set: that check loads all
32
+ Source-2/3 IDs into memory, which on the full ~1.7M-entity test set costs a few GB
33
+ (more when ``candidate_pairs.tsv`` is included). The default run therefore stays fast
34
+ and light and verifies every other rule; it prints a warning noting the check was
35
+ skipped. Pass ``--check-ids`` to turn it on (it reads ``test_source2.tsv`` /
36
+ ``test_source3.tsv`` from ``--test-dir``); a missing/garbage matched ID only lowers
37
+ your score rather than being rejected by the scorer, so this check is a diagnostic,
38
+ not a gate. If ``--check-ids`` runs out of memory, drop ``--candidate`` (the candidate
39
+ cross-check is the biggest memory user, and the matching file is the only one scored).
40
+ """
41
+
42
+ import argparse
43
+ import os
44
+ import sys
45
+
46
+ DELIM = "\t"
47
+ MAX_EXAMPLES = 5 # how many offending IDs to show per issue
48
+ MATCHING_HEADER = ["source1_entity_id", "matched_entity_ids"]
49
+ CANDIDATE_HEADER = ["source1_entity_id", "candidate_entity_ids"]
50
+
51
+
52
+ def read_ids(path):
53
+ """Return the set of first-column entity IDs from a source TSV.
54
+
55
+ The header row is skipped and blank lines are ignored.
56
+ """
57
+ with open(path, encoding="utf-8") as f:
58
+ next(f, None) # skip header
59
+ return {line.split(DELIM, 1)[0].strip() for line in f if line.strip()}
60
+
61
+
62
+ def examples(items):
63
+ """Return a short, human-readable sample of ``items`` for an error message."""
64
+ items = sorted(items)
65
+ shown = ", ".join(items[:MAX_EXAMPLES])
66
+ if len(items) > MAX_EXAMPLES:
67
+ return f"{len(items)} total, e.g. {shown}, ..."
68
+ return shown
69
+
70
+
71
+ def load_match_targets(test_dir, warnings):
72
+ """Return the set of valid S2/S3 match IDs, or ``None`` if unavailable.
73
+
74
+ Only called when ``--check-ids`` is on. When ``test_source2.tsv`` or
75
+ ``test_source3.tsv`` is missing we cannot check that matched IDs exist, so we
76
+ record a warning and return ``None`` to signal that the existence check should be
77
+ skipped.
78
+ """
79
+ targets = set()
80
+ for name in ("test_source2.tsv", "test_source3.tsv"):
81
+ path = os.path.join(test_dir, name)
82
+ if not os.path.isfile(path):
83
+ warnings.append(
84
+ f"{path} not found — skipping the (optional) check that matched "
85
+ f"IDs exist in the test set. Every other rule is still checked. "
86
+ f"This is the lighter-memory mode; provide test_source2/3.tsv to "
87
+ f"enable the ID-existence check."
88
+ )
89
+ return None
90
+ targets |= read_ids(path)
91
+ return targets
92
+
93
+
94
+ def validate_id_list_file(path, expected_header, col_label, required, valid_ids, errors):
95
+ """Validate one results-style TSV (matching or candidate).
96
+
97
+ Applies the shared formatting rules and appends any problems to ``errors``.
98
+ Returns a ``{source1_id: set(matched/candidate ids)}`` mapping, or ``None`` on a
99
+ fatal problem (missing file, empty file, or a broken header) that stops parsing.
100
+ """
101
+ if not os.path.isfile(path):
102
+ errors.append(f"File not found: {path}")
103
+ return None
104
+
105
+ name = os.path.basename(path)
106
+ mapping = {}
107
+ seen, dup_rows, intra_dupes = set(), set(), set()
108
+ self_matches, wrong_prefix, unknown = set(), set(), set()
109
+ n_rows = empties = 0
110
+
111
+ with open(path, encoding="utf-8") as f:
112
+ header = f.readline()
113
+ if not header:
114
+ errors.append(f"{name} is empty.")
115
+ return None
116
+ if DELIM not in header and "," in header: # the #1 mistake: a CSV
117
+ errors.append(
118
+ f"{name}: header has no TAB but contains commas — the file looks "
119
+ "COMMA-separated. Submissions must be TAB-separated (.tsv); "
120
+ "write it with df.to_csv(sep='\\t', index=False)."
121
+ )
122
+ return None
123
+ cols = [c.strip().lower() for c in header.rstrip("\n").split(DELIM)]
124
+ if cols != expected_header:
125
+ errors.append(
126
+ f"{name}: unexpected header {cols}. "
127
+ f"Expected exactly {expected_header} (tab-separated)."
128
+ )
129
+ return None
130
+
131
+ for line_num, line in enumerate(f, start=2):
132
+ s1, tab, rest = line.partition(DELIM)
133
+ if not tab:
134
+ if s1.strip():
135
+ errors.append(
136
+ f"{name}: malformed row (no tab) at line {line_num}: "
137
+ f"{line.rstrip()!r}"
138
+ )
139
+ continue
140
+
141
+ n_rows += 1
142
+ if s1 in seen:
143
+ dup_rows.add(s1)
144
+ seen.add(s1)
145
+
146
+ ids = rest.rstrip("\n").split(",") if rest.strip() else []
147
+ if not ids:
148
+ empties += 1
149
+ mapping[s1] = set()
150
+ continue
151
+ if len(ids) != len(set(ids)):
152
+ intra_dupes.add(s1)
153
+ id_set = set(ids)
154
+ mapping[s1] = id_set
155
+ for mid in id_set:
156
+ if mid.startswith("S1-"):
157
+ self_matches.add(mid)
158
+ elif not mid.startswith(("S2-", "S3-")):
159
+ wrong_prefix.add(mid)
160
+ elif valid_ids is not None and mid not in valid_ids:
161
+ unknown.add(mid)
162
+
163
+ # Aggregate the per-category findings. Each entry is (offenders, message);
164
+ # only non-empty categories become errors.
165
+ findings = [
166
+ (
167
+ dup_rows,
168
+ "{name}: duplicate source1_entity_id row(s): {ex}. "
169
+ "Each S1 entity may appear on only one row.",
170
+ ),
171
+ (
172
+ intra_dupes,
173
+ "{name}: repeated ID inside a {col} list for: {ex}. "
174
+ "No duplicate IDs are allowed within a list.",
175
+ ),
176
+ (
177
+ self_matches,
178
+ "{name}: {col} contains Source-1 IDs (self-matches): {ex}. "
179
+ "Only S2-/S3- IDs are allowed.",
180
+ ),
181
+ (
182
+ wrong_prefix,
183
+ "{name}: {col} contains IDs without an S2-/S3- prefix: {ex}.",
184
+ ),
185
+ (
186
+ unknown,
187
+ "{name}: {col} references IDs not in the test "
188
+ "Source-2/3 files: {ex}.",
189
+ ),
190
+ (
191
+ required - seen,
192
+ "{name}: required S1 entity(ies) missing: {ex}. "
193
+ "Every entity in test_source1.tsv needs a row (empty = no match).",
194
+ ),
195
+ (
196
+ seen - required,
197
+ "{name}: row(s) using an S1 ID that is not in the test set: {ex}.",
198
+ ),
199
+ ]
200
+ for offenders, message in findings:
201
+ if offenders:
202
+ errors.append(message.format(name=name, ex=examples(offenders), col=col_label))
203
+
204
+ print(f" {name}: {n_rows} rows ({empties} empty, {n_rows - empties} non-empty).")
205
+ return mapping
206
+
207
+
208
+ def validate(matching_path, candidate_path, test_dir, check_ids=False):
209
+ """Validate the submission output(s); return ``(errors, warnings)`` lists.
210
+
211
+ ``check_ids`` (``--check-ids``) turns on the optional, memory-heavy check that
212
+ every matched/candidate ID exists in the test Source-2/3 files. It is off by
213
+ default so the common run stays fast and light.
214
+ """
215
+ errors, warnings = [], []
216
+
217
+ source1 = os.path.join(test_dir, "test_source1.tsv")
218
+ if not os.path.isfile(source1):
219
+ errors.append(f"Test source1 file not found: {source1} (check --test-dir).")
220
+ return errors, warnings
221
+ required = read_ids(source1)
222
+ print(f" required S1 entities: {len(required)}")
223
+
224
+ if check_ids:
225
+ valid_ids = load_match_targets(test_dir, warnings)
226
+ if valid_ids is not None:
227
+ print(f" valid S2/S3 match IDs: {len(valid_ids)}")
228
+ else:
229
+ valid_ids = None
230
+ warnings.append(
231
+ "ID-existence check is OFF (the default) — not checking that matched/"
232
+ "candidate IDs exist in the test set. Every other rule is still checked. "
233
+ "Re-run with --check-ids to enable it (needs test_source2/3.tsv; uses "
234
+ "more memory). A nonexistent ID only lowers your score, never rejects "
235
+ "your submission."
236
+ )
237
+
238
+ matched = validate_id_list_file(
239
+ matching_path, MATCHING_HEADER, "matched_entity_ids", required, valid_ids, errors
240
+ )
241
+
242
+ # candidate_pairs.tsv is optional: if it's absent we skip its checks with a
243
+ # warning (it's still expected in your final submission zip). A missing
244
+ # candidate file never fails this run on its own.
245
+ candidate = None
246
+ if candidate_path and os.path.isfile(candidate_path):
247
+ candidate = validate_id_list_file(
248
+ candidate_path, CANDIDATE_HEADER, "candidate_entity_ids",
249
+ required, valid_ids, errors,
250
+ )
251
+ elif candidate_path:
252
+ warnings.append(
253
+ f"{candidate_path} not found — skipping candidate_pairs.tsv checks. "
254
+ "It is optional here, but your final submission zip must include "
255
+ "output/candidate_pairs.tsv."
256
+ )
257
+
258
+ # Soft check: your final matches should come from your blocking candidates.
259
+ # A matched ID absent from candidate_pairs.tsv usually means a pipeline bug,
260
+ # so we warn but never fail on it.
261
+ if matched is not None and candidate is not None:
262
+ offenders = {
263
+ s1 for s1, mids in matched.items() if mids - candidate.get(s1, set())
264
+ }
265
+ if offenders:
266
+ warnings.append(
267
+ f"{len(offenders)} S1 entity(ies) have matched IDs not present in "
268
+ f"candidate_pairs.tsv, e.g. {examples(offenders)}. Final matches "
269
+ "normally come from your blocking candidates — double-check these."
270
+ )
271
+
272
+ return errors, warnings
273
+
274
+
275
+ def main():
276
+ parser = argparse.ArgumentParser(
277
+ description="Validate ML Challenge 2026 submission output files before submitting."
278
+ )
279
+ parser.add_argument(
280
+ "--matching",
281
+ "-m",
282
+ default="output/matching_results.tsv",
283
+ help="Path to matching_results.tsv (default: %(default)s)",
284
+ )
285
+ parser.add_argument(
286
+ "--candidate",
287
+ "-c",
288
+ default=None,
289
+ help="Path to candidate_pairs.tsv "
290
+ "(default: output/candidate_pairs.tsv if it exists).",
291
+ )
292
+ parser.add_argument(
293
+ "--test-dir",
294
+ "-t",
295
+ default="dataset/test",
296
+ help="Folder with test_source1/2/3.tsv (default: %(default)s). "
297
+ "test_source2/3.tsv are only read when --check-ids is given.",
298
+ )
299
+ parser.add_argument(
300
+ "--check-ids",
301
+ action="store_true",
302
+ help="Also check that every matched/candidate ID exists in the test "
303
+ "Source-2/3 files. Off by default (loads all S2/S3 IDs into memory — a few "
304
+ "GB on the full test set). A nonexistent ID only lowers your score, so this "
305
+ "is a diagnostic, not a submission gate.",
306
+ )
307
+ args = parser.parse_args()
308
+
309
+ # candidate_pairs.tsv is optional; default to the conventional path and let
310
+ # validate() skip (with a warning) if the file isn't there.
311
+ candidate_path = args.candidate or "output/candidate_pairs.tsv"
312
+
313
+ print("ML Challenge 2026 — submission validator")
314
+ print(f" test dir: {args.test_dir}")
315
+ try:
316
+ errors, warnings = validate(
317
+ args.matching, candidate_path, args.test_dir, check_ids=args.check_ids
318
+ )
319
+ except UnicodeDecodeError:
320
+ print()
321
+ print("FAIL — 1 issue(s) to fix before submitting:")
322
+ print(
323
+ f" 1. A file is not valid UTF-8 text (most likely {args.matching} or "
324
+ f"{candidate_path}). Re-save it as a plain UTF-8, tab-separated .tsv — "
325
+ "not cp1252/Latin-1, and not a compressed or binary file (.gz/.xlsx/"
326
+ ".parquet) renamed to .tsv. In pandas: "
327
+ "df.to_csv(path, sep='\\t', index=False, encoding='utf-8')."
328
+ )
329
+ return 1
330
+ except OSError as exc:
331
+ print()
332
+ print("FAIL — 1 issue(s) to fix before submitting:")
333
+ print(f" 1. Could not read a file: {exc}.")
334
+ return 1
335
+
336
+ print()
337
+ for warning in warnings:
338
+ print(f"WARNING: {warning}")
339
+ if errors:
340
+ print(f"FAIL — {len(errors)} issue(s) to fix before submitting:")
341
+ for i, error in enumerate(errors, 1):
342
+ print(f" {i}. {error}")
343
+ return 1
344
+ print("PASS — no blocking issues found. Safe to submit.")
345
+ return 0
346
+
347
+
348
+ if __name__ == "__main__":
349
+ sys.exit(main())
model-4/DESIGN.md ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Business Entity Resolution — Multi-Embedding Pipeline
2
+
3
+ For every Source 1 business, find the Source 2 / Source 3 records that describe the same
4
+ real-world business, maximizing macro-averaged F0.5 (precision-weighted, singletons included).
5
+ Training data covers US and India; the test set adds France, unseen at training time — nothing
6
+ in this pipeline is hard-coded to a specific country.
7
+
8
+ This document is self-contained: everything needed to understand and run this pipeline is here,
9
+ no other project or prior approach required.
10
+
11
+ ---
12
+
13
+ ## 1. Problem recap
14
+
15
+ - Three sources of business records (`S1-`, `S2-`, `S3-` entity IDs), fields: `business_name`,
16
+ `business_address`, `country`. Source 1 is the deduplicated reference; for every S1 entity we
17
+ must find all S2/S3 records describing the same business (zero, one, or many).
18
+ - **Metric:** F0.5 per S1 entity, macro-averaged across all S1 entities. A singleton (no true
19
+ match) scores 1.0 for a correct empty prediction and 0.0 for any predicted match. F0.5 weights
20
+ precision twice as heavily as recall.
21
+ - **Hard rules:** models must be MIT/Apache-2.0 licensed and ≤8B parameters; no external lookups,
22
+ APIs, or geocoding; country is an open string set, never filtered or one-hot encoded; ground
23
+ truth learning (abbreviation maps, calibration, thresholds) must never leak across folds.
24
+
25
+ ## 2. Architecture
26
+
27
+ ```
28
+ raw TSVs (S1/S2/S3, train + test)
29
+ │
30
+ ▼
31
+ [0] Integrity manifest — row counts, ID prefixes, duplicate IDs, embedding-row alignment
32
+ for every embedding source, ground-truth target coverage, S2/S3 multi-owner check
33
+ │
34
+ ▼
35
+ [1] Normalisation — casefold, accent-fold, punctuation/number normalisation, legal-suffix and
36
+ abbreviation maps learned per fold from training positives only. Two parallel text views are
37
+ kept everywhere:
38
+ • rule-free (core, addr, name_plain) → used for every FEATURE, so all countries
39
+ (including unseen France) look the same to the model
40
+ • rule-applied (bcore, baddr) → used ONLY for blocking/retrieval, where the
41
+ learned abbreviation expansions help recall
42
+ France gets label-free suffix discovery at test time (no supervised France examples exist).
43
+ │
44
+ ▼
45
+ [2] Splits — disjoint S1 entity groups: ce (learns rules + trains the cross-encoder), stack
46
+ (trains LightGBM, calibration, decision tuning), val (evaluation only). Split A: random
47
+ mixed-country groups. Split B: leave-one-country-out (train US → validate India, and the
48
+ reverse) as the proxy for how the model will behave on unseen France. Every split searches
49
+ the SAME shared S2+S3 pool — never partitioned — so a split's own true matches can never be
50
+ stranded on the wrong side, and validation faces the same one-shared-pool competition test
51
+ inference will face.
52
+ │
53
+ ▼
54
+ [3] Candidate generation — multiple independent retrievers, fused by rank
55
+ ┌───────────────────────────────────────────────────────────────────┐
56
+ │ per-country partitioned, run independently over the S2∪S3 pool: │
57
+ │ │
58
+ │ • Dense retriever: intfloat/multilingual-e5-large (MIT, 560M) │
59
+ │ • Dense retriever: BAAI/bge-m3 (MIT, 568M) │
60
+ │ • Lexical retriever: inverted index over rare name tokens, │
61
+ │ name prefixes, and address tokens (with learned abbreviation │
62
+ │ maps applied to widen recall) │
63
+ │ │
64
+ │ each is an EXACT cosine top-K search (GPU, bounded memory) or │
65
+ │ exact inverted-index lookup — no approximate search │
66
+ └───────────────────────────────────────────────────────────────────┘
67
+ │
68
+ ▼ reciprocal-rank fusion (RRF) decides which candidates SURVIVE THE CAP —
69
+ RRF(c) = Σ over retrievers that found c of 1 / (k0 + rank)
70
+ capped at ~100-150 candidates per S1 (tuned from recall-vs-K probes)
71
+ │
72
+ ▼ every surviving candidate keeps EACH retriever's own score, rank, gap-to-best,
73
+ and percentile as SEPARATE features — RRF only gates survival, the model never
74
+ sees a blended score
75
+ │
76
+ ▼
77
+ [4] Pair features — ~35 country-agnostic features on the rule-free text:
78
+ Retrieval: per-source score/rank/gap/percentile (e5, bge, lexical),
79
+ retrieval_support_count (how many of the 3 independently found it)
80
+ Name: Jaro-Winkler, Levenshtein, token-sort/set ratio, plain-name ratio, token
81
+ Jaccard, dictionary-free abbreviation match, legal-suffix agreement
82
+ Address: ratio/token-set/partial similarity, token Jaccard, exact house/unit
83
+ alnum-token overlap, numeric-set overlap
84
+ Contradictions: graded, 3-state (missing/uncertain ≠ conflict — never conflated):
85
+ general numeric conflict, postal-code-length conflict, house-number
86
+ conflict
87
+ Cross-source: triangle S2↔S3 corroboration for the same S1 (feature only — never
88
+ lets the decision layer add a candidate blocking never retrieved)
89
+ Context: same-country flag, name/address length, missingness flags, script flags
90
+ │
91
+ ▼
92
+ [5] Cross-encoder — microsoft/mdeberta-v3-base (MIT), fine-tuned on ce-split pairs (positives +
93
+ hard negatives mined from blocking, both label orders, bf16). Its probability is stacked into
94
+ LightGBM as a feature, computed out-of-fold on the training side to avoid leakage.
95
+ │
96
+ ▼
97
+ [6] Stacker — LightGBM, grouped K-fold on the stack split, early stopping on an inner holdout,
98
+ fold-averaged predictions. The variant (with/without cross-encoder) and decision family
99
+ (threshold vs. expected-F0.5) are chosen on the stack partition's own score only — the val
100
+ part is never used for any selection, only for reporting.
101
+ │
102
+ ▼
103
+ [7] Calibration — cross-fitted isotonic regression, fit on the FULL out-of-fold stack scores,
104
+ decoupled from conflict resolution (calibrating after arbitration on a biased survivor set
105
+ silently miscalibrates scores — this pipeline avoids that ordering entirely).
106
+ │
107
+ ▼
108
+ [8] Decision — exact expected-F0.5 selection per S1, then one-owner arbitration. For each S1,
109
+ for every candidate-set size k = 0, 1, 2, … (candidates sorted by calibrated probability),
110
+ the EXACT expected F0.5 of taking the top-k is computed via a Poisson-binomial dynamic
111
+ program (k=0 is how singletons earn their 1.0). Each entity independently picks its
112
+ best k; when two S1 entities claim the same S2/S3 record, the higher-scoring claim keeps it
113
+ and the loser re-decides without that candidate — iterated to a fixpoint, never a one-shot
114
+ greedy reservation.
115
+ │
116
+ ▼
117
+ [9] Output + validation — matching_results.tsv and candidate_pairs.tsv, with in-script
118
+ duplicate/subset/ID checks, plus an optional call to the organizers' validate_submission.py
119
+ as the final gate before upload.
120
+ ```
121
+
122
+ ## 3. Why two dense retrievers, fused by rank rather than merged score
123
+
124
+ Cosine similarity from two different embedding models isn't on the same scale — a 0.7 from
125
+ e5-large and a 0.7 from bge-m3 don't mean the same thing. Reciprocal-rank fusion sidesteps that:
126
+ it only asks "how highly did each retriever rank this candidate," which is scale-free, and uses
127
+ that purely to decide which candidates make it into the capped set that gets fully featurized and
128
+ scored. Once a candidate survives, the actual LightGBM/cross-encoder stack sees each retriever's
129
+ raw score, rank, and per-query percentile as independent features, plus how many retrievers found
130
+ it at all. This lets the model learn its own trust weighting per model (e.g. relying more on one
131
+ embedding family for Indian transliterated names, the other for French text) rather than that
132
+ being decided upfront by a hand-tuned blend.
133
+
134
+ ## 4. Data integrity
135
+
136
+ Before any training or test run, `src/manifest.py` reports, for every raw file and every
137
+ embedding source: row count, unique-ID count, duplicate IDs, ID-prefix correctness, and whether
138
+ the embedding manifest's row count matches the data file. It also flags whether any S2/S3 ID is
139
+ claimed as a true match by more than one S1 entity in the ground truth — if that count is ever
140
+ nonzero, entity-based fold splitting is no longer sufficient and folds need to be built from
141
+ connected components of the full label graph instead of individual S1 groups.
142
+
143
+ ## 5. Run
144
+
145
+ 1. Fill `PATHS` in `src/config.py` — the four raw source files, ground truth, and a work
146
+ directory. Parquet or TSV both work; only `entity_id, business_name, business_address,
147
+ country` are read.
148
+ 2. Point `CFG["emb_sources"]` at your two pre-computed embedding sets (same shard/manifest
149
+ format for both — `<dir>/<split>/source{1,2,3}/manifest.json` + `part-*.npy` files):
150
+ ```python
151
+ CFG["emb_sources"] = {
152
+ "e5": "/path/to/e5_multilingual_large",
153
+ "bge": "/path/to/bge_m3",
154
+ }
155
+ ```
156
+ Every run verifies each source's row count and unit-norm vectors, and — on training data —
157
+ that true pairs score far above random pairs; on test data, that nearest neighbours usually
158
+ share a name word. A run stops rather than silently continuing if any source doesn't line up.
159
+ 3. Run the integrity manifest first:
160
+ ```bash
161
+ python src/manifest.py --data-dir <folder with train_source1/2/3.tsv> --split train \
162
+ --emb-dir e5=<e5 train dir> --emb-dir bge=<bge train dir> \
163
+ --ground-truth <folder>/train_ground_truth.tsv
164
+ ```
165
+ Fix anything it reports before proceeding.
166
+ 4. Train + validate:
167
+ ```bash
168
+ python src/run_train_eval.py --n-ce 20000 --n-stack 10000 --n-val 10000 # quick run
169
+ python src/run_train_eval.py # default sizes
170
+ python src/run_train_eval.py --split B # US -> India transfer
171
+ python src/run_train_eval.py --split B --train-country india --val-country us
172
+ ```
173
+ Results: `work/results_<run>/report.md` (+ `metrics.json`, `errors.tsv`, `fn_stages.tsv`,
174
+ `feature_importance.tsv`, `resolved_config.json`, `split_ids.json`).
175
+ Trained pieces: `work/models_<run>/` and `work/ce_model_<run>/`.
176
+ 5. Test inference (writes the submission files):
177
+ ```bash
178
+ python src/manifest.py --data-dir <test dir> --split test \
179
+ --emb-dir e5=<e5 test dir> --emb-dir bge=<bge test dir>
180
+ python src/run_test.py --models work/models_A --test-dir <folder with test_source1/2/3> \
181
+ --emb-sources e5=<e5 test dir> bge=<bge test dir> \
182
+ --validator ../utils/validate_submission.py --validator-test-dir <folder with test TSVs>
183
+ ```
184
+ Output: `output/matching_results.tsv` (the file scored on the leaderboard) and
185
+ `output/candidate_pairs.tsv` (the blocking audit trail).
186
+
187
+ ## 6. Rules compliance
188
+
189
+ Models: `intfloat/multilingual-e5-large` (MIT, 560M) and `BAAI/bge-m3` (MIT, 568M) for
190
+ retrieval embeddings, `microsoft/mdeberta-v3-base` (MIT, ~278M) as the cross-encoder, LightGBM
191
+ (MIT) as the tabular scorer — all well under the 8B parameter limit. No external data,
192
+ dictionaries, APIs, or geocoding: every suffix and abbreviation rule is learned from the provided
193
+ training labels, and country is only ever compared for string equality.
194
+
195
+ ## 7. Code layout
196
+
197
+ ```
198
+ src/
199
+ ├── config.py # paths + tunable settings (fill in before running)
200
+ ├── defaults.py # settings not in config.py; config.py values always win
201
+ ├── manifest.py # pre-flight integrity report (run before any training/test run)
202
+ ├── data.py # loading, ground-truth attachment, sanity checks, splits
203
+ ├── text_utils.py # normalisation, number/alnum-token extraction, abbreviation matching
204
+ ├── transforms.py # per-fold learned rules; rule-free vs rule-applied text views
205
+ ├── embeddings.py # loads + aligns pre-computed embedding shards for one source
206
+ ├── blocking.py # dense (per source) + lexical retrieval, RRF-fused candidate union
207
+ ├── features.py # pair feature computation
208
+ ├── cross_encoder.py # mDeBERTa fine-tuning + inference
209
+ ├── stacker.py # LightGBM K-fold training/inference
210
+ ├── decide.py # calibration + exact expected-F0.5 decision + one-owner arbitration
211
+ ├── evaluate.py # macro F0.5 (matches the problem statement's worked example)
212
+ ├── run_train_eval.py # end-to-end training + validation
213
+ └── run_test.py # test inference + submission file writing
214
+ ```
215
+
216
+ ## 8. Relationship to any other approach
217
+
218
+ This pipeline is fully standalone: nothing here depends on, imports from, or assumes the
219
+ existence of any other codebase or prior attempt at this problem. It can be read, run, and
220
+ audited entirely on its own.
model-4/Documentation_template.md ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ML Challenge 2026: Business Entity Resolution Solution Template
2
+
3
+ **Team Name:** [Your Team Name]
4
+ **Team Members:** [List all team members]
5
+ **Submission Date:** [Date]
6
+
7
+ ---
8
+
9
+ ## 1. Executive Summary
10
+
11
+ A two-stage blocking-and-ranking pipeline. Three independent retrievers (multilingual-e5-large, bge-m3 and an
12
+ IDF-weighted lexical index), run per country, are fused by reciprocal rank. Two LightGBM stages score the
13
+ candidates: a cheap stage-1 model over all fused candidates, then a stage-2 model over the top 16 that adds
14
+ a fine-tuned mDeBERTa-v3 cross-encoder. Each S1 entity's final set is chosen from calibrated probabilities
15
+ with one-owner arbitration. Every text rule is country-agnostic and label-free (legal forms are discovered
16
+ from unlabeled names), so the unseen country, France, is processed exactly like US and India.
17
+ Validation macro F0.5: **0.98298**.
18
+
19
+ ---
20
+
21
+ ## 2. Methodology
22
+
23
+ ### 2.1 Problem Analysis
24
+
25
+ - **Scale:** train 2.21M S1 / 10.32M S2+S3; test 1.73M S1 / 9.97M S2+S3 (France: 259k S1, 1.43M S2+S3).
26
+ - **Label structure:** 5.6% of S1 are singletons; the rest have 1–11 matches (typically 2–4). No S2/S3 record
27
+ belongs to two S1 entities, so a one-owner constraint is valid.
28
+ - **Hard negatives dominate:** the same street with a neighbouring house number (`17` vs `18 R DE VALMY`),
29
+ and the same address with one name word swapped (`Supérieure Sante EURL` vs `Supérieure Club EURL`).
30
+ - **Name noise:** legal forms at the end, the start or in brackets (`SARL X`, `[SAS] X`, `S.A.S.`), glued web
31
+ forms (`shidokanecolesarl.com`), trade names (`X trading as Y`), typos, Indian-script transliterations
32
+ (11% of test S2 names are Devanagari, Telugu, Tamil etc.).
33
+ - **Address noise (France):** 40% of "Rue" appear as `R`/`R.`; `BD`, `AV`/`AVE`, `ALL`, `IMP`, `RTE`, `PL`,
34
+ `CH`; the region (S1) vs the department (S2/S3); `N°`, `Nº`, `#`, `Bis`, `3B`, ranges `2 - 6`; postcodes in
35
+ under 0.5% of records.
36
+
37
+ ### 2.2 Solution Strategy
38
+
39
+ **Approach Type:** Blocking + two-stage classifier cascade (LightGBM + cross-encoder), calibrated
40
+ expected-F0.5 / threshold decision with one-owner arbitration.
41
+ **Core Innovation:** label-free, country-agnostic normalisation (legal forms discovered from unlabeled
42
+ text using mutual exclusivity, abbreviation matching by a generic prefix/subsequence rule, segment-aware
43
+ address parsing with house-number states), so an unseen country needs no special handling.
44
+
45
+ ---
46
+
47
+ ## 3. Candidate Generation (Blocking)
48
+
49
+ - **Blocking keys used:**
50
+ - Dense: exact cosine top-100 per country with multilingual-e5-large and bge-m3 (GPU, no approximation).
51
+ - Lexical: exact IDF-weighted cosine top-100 per country over name tokens, 5-character name prefix,
52
+ street tokens, locality tokens, house-number+street composites, postcodes and alphanumeric unit tokens.
53
+ - Reciprocal-rank fusion (k0 = 60) decides which candidates survive a cap of K = 64 per S1 (smallest K
54
+ reaching 99.5% of the recall at K = 100). The model still sees each retriever's own score, rank and gap.
55
+ - A stage-1 LightGBM keeps the top 16 candidates per S1 (p ≥ 0.003). This final set is
56
+ `candidate_pairs.tsv`.
57
+ - **Candidate pairs generated:** 110,882,816 test pairs after fusion; **9,980,755 test pairs (5.8 per S1)** in `candidate_pairs.tsv`.
58
+ - **How you ensured true matches were not lost:** recall was measured on held-out train groups
59
+ (1.04M true pairs). Fused recall at K = 64: **98.50%** (US 99.07%, India 97.65%), above any
60
+ single retriever (e5 97.4%, bge 96.3%, lexical 91.1% at 100). The stage-1 filter keeps 99.92% of the
61
+ true pairs that blocking found.
62
+
63
+ ---
64
+
65
+ ## 4. Matching Model
66
+
67
+ **Features used (~70, all on rule-free text, no country branching):**
68
+ - Name features: ratio, Jaro-Winkler, Levenshtein, token-sort / token-set / partial ratio on the name with
69
+ legal forms removed; full-name ratio; no-space ratio and partial ratio (glued / web-style names);
70
+ abbreviation- and typo-aware token coverage in both directions; token Jaccard; counts of unmatched
71
+ tokens; containment (trade names); acronym match; first-token match; legal-form state (none / one missing
72
+ / same / compatible / conflict).
73
+ - Address features: full-address ratio / token-set / partial / Jaccard; street-segment abbreviation-aware
74
+ coverage, ratio and no-space ratio; locality coverage (does each side's city or region appear in the
75
+ other's address); house-number state (equal / suffix differs / range contains / conflict / missing) and
76
+ log distance; numeric-set and postcode states; alphanumeric unit-token state.
77
+ - Other: per-retriever score, rank and gap; RRF score and rank; retriever support count; exact e5 / bge
78
+ cosine with rank and gap inside the S1; S2↔S3 corroboration (max cosine to a top-10 candidate from the
79
+ other source, near-duplicate count); stage-1 probability, rank and gap; cross-encoder logit, rank and gap;
80
+ non-Latin-script and empty-address flags; candidate source.
81
+
82
+ **Model type:** LightGBM (5 grouped folds by S1) at two stages, with a fine-tuned
83
+ `microsoft/mdeberta-v3-base` cross-encoder (MIT, 278M) whose score is a stage-2 feature. The
84
+ cross-encoder is trained on a disjoint S1 split, so its scores on the stack, validation and test sets
85
+ are out of sample.
86
+ **Threshold selection method:** cross-fitted isotonic calibration on stack-split out-of-fold scores.
87
+ Exact expected-F0.5 per S1 (Poisson-binomial DP) and a threshold grid are both evaluated on the stack
88
+ split only; the best is used. Chosen: **cross-encoder variant + exact expected-F0.5 (stack 0.98248 vs best threshold 0.98223 at t=0.70)**. The validation split is used only for reporting.
89
+
90
+ ---
91
+
92
+ ## 5. Results & Error Analysis
93
+
94
+ - **F_0.5 Score (macro):** **0.98298** on 100k held-out S1 (US 0.98325, India 0.98258).
95
+ - **Leave-one-country-out proxy:** with stage-1 features only (no cross-encoder), a model trained on one country and applied to the other loses 3-12 points: US->US 0.973 vs US->India 0.854; India->India 0.965 vs India->US 0.934. The loss is largest going into India (Indian scripts, landmark addresses unseen in US data); France, being Latin-script with structured addresses, is closer to the milder case, and the final model is trained on both countries.
96
+ - **France (no labels):** predicted singleton rate 5.99% and mean matches 3.15, vs US
97
+ 5.83%/3.32 and India 5.93%/3.29; nearest-neighbour name-word agreement
98
+ 98.96% (US 98.64%, India 93.32%).
99
+ - **Common false positives (wrong merges):** a singleton S1 linked to a near-identical business: same legal form and similar name on a nearby street or house number (e.g. `Graham Truist PC`, `Steinmann National Inc.`). Among the 2,000 lowest-scoring validation entities, 97 are singletons wrongly merged and only 10 received exclusively wrong matches; the rest are partial.
100
+ - **Common false negatives (missed matches):** (1) true matches that retrieval never surfaced, typically very short/generic names (`Amber`) or long landmark-heavy Indian addresses. They cap recall at 98.4%. (2) Partial matches, where the model keeps the confident records and drops heavily corrupted ones (glued names, transliterated script, missing house number). 1,646 of the 2,000 worst entities are partial rather than wrong.
101
+
102
+ ---
103
+
104
+ ## 6. Conclusion
105
+
106
+ A retrieval-heavy, precision-first cascade reaches 0.983 macro F0.5 on held-out US/India data. Its fused blocking recall is 98.5%, and its expected-F0.5 decision is calibrated: predicted singleton rates match the true 5.6%. Because every normalisation rule is label-free and country-agnostic, France receives exactly the same processing, and its test-time prediction profile (6.0% singletons, 3.15 matches per entity) matches the labelled countries. Lessons: exact expected-F0.5 beats a global threshold, a cross-encoder on a small top-K set gives most of the lift, and on shared hardware the pipeline had to be engineered for resumability and GPU/CPU placement as much as for accuracy.
107
+
108
+ ---
109
+
110
+ ## Appendix
111
+
112
+ ### A. Code Artefacts
113
+
114
+ `code/business_entity_resolution/src/`: `run_pipeline.py` is the entry point, a resumable dependency-graph
115
+ runner. `scripts/run.sh start` reproduces both output files from the raw data: normalisation → retrieval →
116
+ fusion → features → stage 1 → cross-encoder → stage 2 → decision → `output/`. Each stage checkpoints per
117
+ shard and a restart resumes. See `IMPLEMENTATION.md` for the stage list, GPU placement and settings.
118
+
119
+ ### B. Additional Results
120
+
121
+ | Stage | Metric | Value |
122
+ |---|---|---|
123
+ | Retrieval | fused recall@64 / @100 | 98.50% / 98.78% |
124
+ | Retrieval | e5 / bge / lexical recall@100 | 97.36% / 96.31% / 91.06% |
125
+ | Stage 1 | AUC (stack OOF / val) | 0.99977 / 0.99978 |
126
+ | Stage-2 set | true pairs kept (vs blocking) | 99.92% |
127
+ | Cross-encoder | dev AUC (580k pairs, 4,529 steps) | 0.99964 |
128
+ | Stage 2 | macro F0.5 (val, 100k S1) | 0.98298 |
129
+ | Top features | gain | cross-encoder logit, stage-1 prob, their gaps to the S1's best, RRF rank, triangle cosine |
model-4/IMPLEMENTATION.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # model-4 — implementation notes and run guide
2
+
3
+ `DESIGN.md` is the original design (it was the README before 2026-09-27). This file describes the pipeline **as implemented** in `src/`, how to run and resume
4
+ it, and every place the implementation departs from the design (with the reason).
5
+
6
+ ## Run
7
+
8
+ ```bash
9
+ cd /scratch/user4/dai/model-4
10
+ scripts/run.sh start # run / resume the whole pipeline, detached (survives SSH drops)
11
+ scripts/run.sh status # per-stage state: done / pending / FAILED xN
12
+ scripts/run.sh logs # orchestrator log; scripts/run.sh tail <stage> for one stage
13
+ scripts/run.sh stop # stop everything (all shards written so far are kept)
14
+ M4_SMOKE=1 scripts/run.sh start # small end-to-end check (3k/3k/2k train S1, 5k test S1) in work_smoke/
15
+ ```
16
+
17
+ Outputs: `output/matching_results.tsv` (leaderboard file) and `output/candidate_pairs.tsv`.
18
+ Reports: `work/reports/` — `report.md` (validation F0.5), `blocking_recall.json`, `s2set_train.json`,
19
+ `split_B.json` (leave-one-country-out), `test_report.json` (per-country prediction stats, shift flags,
20
+ validator output), `feature_importance.tsv`, `val_errors.tsv`, `manifest.json`.
21
+
22
+ Environment: `.venv` (Python 3.12) chains onto `bge-out/.venv` and adds `anyascii`; pinned versions in
23
+ `requirements.txt`. Paths and settings live in `src/config.py`.
24
+
25
+ ## Fail-proofing
26
+
27
+ - **Stages are separate processes** run by `src/run_pipeline.py` over a dependency graph. A stage writes
28
+ `work/state/<stage>.done` only on success; a restart skips finished stages.
29
+ - **Sharded checkpoints.** Retrieval, fusion, cosine, features, stage-1 scoring and cross-encoder inference
30
+ write one file per 25k-query shard (dense retrieval: per shard × country). All writes are atomic
31
+ (temp file + rename), so a crash never leaves a half-written file that looks finished.
32
+ - **Cross-encoder training** checkpoints model/optimizer/scheduler every 1,000 steps and resumes at the exact
33
+ step with the same data order. A non-finite gradient aborts the stage instead of saving a corrupted checkpoint.
34
+ - **LightGBM** fold models are saved as they finish; a rerun loads them instead of retraining.
35
+ - **Retries.** A failed stage is retried up to 3 times with back-off; only its dependents are blocked.
36
+ - **Shared-GPU patience.** CUDA out-of-memory (other users filling the card) makes the stage free its cache,
37
+ wait 60 s and retry, for up to 3 h, instead of failing. Batch sizes halve automatically on OOM.
38
+ - **Single runner.** A file lock prevents two orchestrators on the same work dir.
39
+
40
+ ## GPU placement
41
+
42
+ | Stage | Device | Memory needed | Why |
43
+ |---|---|---|---|
44
+ | dense retrieval (e5, bge; train, test) | GPU | ~16 GB | exact cosine top-100; one country's pool on the card at a time |
45
+ | exact cosine + S2↔S3 triangle | GPU | ~6 GB | pool stays in RAM; only each batch's candidates go to the GPU |
46
+ | cross-encoder training | GPU | ~16 GB | mDeBERTa-v3-base, bf16 autocast, fp32 master weights |
47
+ | cross-encoder inference (test) | **both GPUs** | ~8 GB each | shards split across every GPU with room |
48
+ | lexical retrieval, fusion, features, LightGBM, decision | CPU | — | 24 worker processes (the server is shared) |
49
+
50
+ The scheduler sends each GPU stage to the card with the most free memory (from `nvidia-smi`, minus memory
51
+ reserved by our stages started in the last 5 minutes), runs the train and test branches concurrently,
52
+ and waits when neither card has room.
53
+
54
+ ## Pipeline as built
55
+
56
+ 1. **manifest** — row/ID/prefix checks, embedding row-order and unit-norm checks, ground-truth coverage,
57
+ S2/S3 multi-owner check (0 → one-owner arbitration valid), true-vs-random pair cosine per embedding.
58
+ 2. **prep** — normalises all 24M records from the raw name/address text (the processed parquet columns are
59
+ not used: they leave `R`/`BD` unexpanded, miss prefix legal forms like `SCI X`, and mis-detect postcodes).
60
+ 3. **splits** — disjoint S1 groups: ce 120k, stack 200k, val 100k (random, mixed-country).
61
+ 4. **retrieval** — e5-large and bge-m3 exact top-100 per country; lexical top-100 over an IDF-weighted
62
+ sparse index (name tokens, 5-char name prefix, street tokens, locality tokens, house-number+street
63
+ composites, postcodes, alphanumeric unit tokens), per country.
64
+ 5. **fusion** — RRF (k0=60) decides survival; cap K chosen automatically as the smallest K reaching 99.5% of
65
+ the recall at K=100 (smoke run: K=64). Each retriever's own score/rank/gap stays a separate feature.
66
+ 6. **cosine + triangle** — exact e5/bge cosine for every candidate (including ones a retriever missed), and
67
+ S2↔S3 corroboration: max cosine to a top-10 candidate from the *other* source, and near-duplicate count.
68
+ 7. **features** — ~70 country-agnostic features (see below).
69
+ 8. **stage 1** — LightGBM, 5 grouped folds on the stack split (negatives beyond RRF rank 32 subsampled),
70
+ out-of-fold scores for stack, fold-averaged for val/test.
71
+ 9. **stage-2 set** — per S1, the top 16 stage-1 candidates with p ≥ 0.003. **This is `candidate_pairs.tsv`**:
72
+ the exact set the final model scores.
73
+ 10. **cross-encoder** — mDeBERTa-v3-base fine-tuned on ce-split positives + hard negatives (both text orders),
74
+ scored on the stage-2 set. The ce split is disjoint from stack/val, so its scores are leak-free.
75
+ 11. **stage 2** — LightGBM with and without the cross-encoder features, 5 grouped folds; cross-fitted isotonic
76
+ calibration; decision family (exact expected-F0.5 vs. threshold grid) chosen on the stack split only;
77
+ validation is reported, never used for selection.
78
+ 12. **decision** — per S1, then one-owner arbitration to a fixpoint (higher-probability claim keeps a
79
+ contested record; the loser re-decides without it).
80
+ 13. **output** — both TSVs, in-script checks (one row per S1, no duplicates, S2/S3 only, matches ⊆
81
+ candidates), the official validator with `--check-ids`, and per-country prediction statistics.
82
+ 14. **eval_B** — leave-one-country-out proxy for France (US→India, India→US), report only.
83
+
84
+ ## Features (all on rule-free text, no country branching)
85
+
86
+ - **Retrieval:** e5/bge/lexical score, rank, gap to best; RRF score and rank; how many retrievers found it;
87
+ exact e5/bge cosine, its gap and rank within the S1; best cosine of the S1.
88
+ - **Name** (legal forms removed): ratio, Jaro-Winkler, Levenshtein, token-sort, token-set, partial ratio,
89
+ full-name ratio; **no-space ratio / partial** (`shidokanecolesarl.com` vs `Shidokan Ecole SARL`);
90
+ abbreviation/typo-aware token coverage both ways; token Jaccard; unmatched-token counts (catches
91
+ descriptor words like `Distribution`); containment (trade-name / DBA); acronym match; first-token match;
92
+ legal-form state (none / one missing / same / compatible e.g. `pvt ltd`~`private limited` / conflict).
93
+ - **Address:** full-address ratio / token-set / partial / Jaccard; **street** abbreviation-aware coverage
94
+ both ways, street ratio and no-space ratio; **locality coverage** — does each side's city/region appear in
95
+ the other's address (handles region-vs-department and reordering); **house-number state** (both missing /
96
+ one missing / equal / same base, different suffix `3B`≠`3` / range contains `2 - 6` / conflict) and
97
+ log distance; numeric-set Jaccard and state; postcode state (incl. length conflict); alphanumeric unit
98
+ token state (`C-199`, `248/A`).
99
+ - **Context:** token counts, empty-address and non-Latin-script flags per side, candidate source (S2/S3),
100
+ candidate count of the S1.
101
+
102
+ ## Departures from the README design (and why)
103
+
104
+ | README | Implemented | Reason |
105
+ |---|---|---|
106
+ | Legal-suffix / abbreviation maps learned from training positives | Legal forms discovered **label-free per country** from all names (boundary position + frequency + mutual exclusivity + neighbour-diversity); abbreviations matched by a generic prefix/subsequence rule | France has no labels. A frequency-only rule would strip descriptors (`groupe`, `distribution`) and `notre`/`pierre`; the exclusivity and neighbour tests reject them. Same treatment for every country, so nothing leaks across folds |
107
+ | Cross-encoder probability as a feature for every candidate | **Two-stage cascade**: cheap stage-1 LightGBM on all K candidates → top-16 → cross-encoder → stage-2 LightGBM | 1.73M test S1 × 64 = 111M pairs is days of mDeBERTa on shared GPUs; the top-16 set keeps 99.7% of blocking's true matches (smoke) |
108
+ | Cross-encoder OOF on the training side | CE trained on the disjoint **ce split**; stack/val/test scores are all out-of-sample | Same leakage guarantee, one training run instead of K |
109
+ | Cross-encoder on all ce pairs | Capped at 600k sampled pairs, batch 128, 1 epoch | ~107 pairs/s on the shared GPU; the CE is one feature among ~70 |
110
+ | Split B trains the full stack | Split B = stage-1-style model without the cross-encoder | A CE trained on both countries would leak the held-out country |
111
+ | Same-country flag feature | Dropped | Retrieval is per-country, so it is always 1 |
112
+ | e5-small embeddings | Not used | Only 8,000 smoke-run query vectors exist; e5-large + bge-m3 are complete |
113
+
114
+ ## Results (full run, 2026-09-26)
115
+
116
+ - Validation macro F0.5 **0.98298** (US 0.98325, India 0.98258); chosen: cross-encoder variant + exact expected-F0.5.
117
+ - Blocking recall@64 98.50%; stage-2 set keeps 99.92% of blocking's true pairs (5.2 candidates per S1).
118
+ - Test: France 5.99% predicted singletons / 3.15 matches per S1 (US 5.83% / 3.32, India 5.93% / 3.29); no
119
+ country-shift flags; official validator with `--check-ids`: PASS.
120
+
121
+ ## GPU helpers added during the full run
122
+
123
+ - `src/gpu_lgb.py` — exact LightGBM tree evaluation on the GPU (same trees and decision rule; float32
124
+ thresholds rounded down so `x <= t32` equals `x <= t` for float32 features). Verified bit-identical to
125
+ `Booster.predict` on 200k rows (max |diff| 8.9e-16).
126
+ - `src/stage1_test_gpu.py` — fills stage-1 test shards on the GPUs from the end while the CPU stage fills
127
+ from the front (the CPU was overloaded by other users; this cut ~2 h). `python stage1_test_gpu.py verify`.
128
+ - `src/ce_infer_help.py` — work-stealing helper for cross-encoder inference when one GPU is much busier.
129
+ - Cross-encoder workers now re-check a shard before scoring it (a helper may have finished it).
130
+
131
+ ## Paths moved on 2026-09-26
132
+
133
+ Another user of the `user4` account reorganised `/scratch/user4/dai/dataset/` during the run (the raw TSVs
134
+ and `dataset/utils/` are gone; the parquet files are now in `dataset/og/`, same SHA-256). `config.py` now
135
+ reads `dataset/og/`, and the validator plus its test TSVs live inside model-4 (`utils/`, `work/test_tsv/`).
136
+
137
+ ## Bugs found and fixed during the smoke run (for the record)
138
+
139
+ - transformers 5 loads mDeBERTa in **fp16** by default → AdamW on fp16 weights gave NaN after step 1.
140
+ Now loaded with `dtype=torch.float32` (bf16 autocast for speed), plus a non-finite-gradient guard.
141
+ - Lexical query matrix was built with CSR arguments in the wrong order (scores ~18,000) — fixed, re-run.
142
+ - `N°33` did not parse as house number 33 (`°` → `" o "`); now `"o "`.
143
+ - rapidfuzz on numpy object arrays was ~10× slower than on lists; metric used `np.isin` against 7.6M
144
+ ground-truth keys per call (10 s) → binary search on a cached sorted array (0.03 s).
model-4/README.md ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # model-4 — Business Entity Resolution
2
+
3
+ Status on 2026-09-27: **finished**. The full training, validation and test run completed at 21:44 on
4
+ 2026-09-26. The submission in `output/` passes the organisers' validator with `--check-ids`. Every number
5
+ below was re-read from `work/reports/`, `work/models/`, the stage logs and the output files on 2026-09-27.
6
+
7
+ | | |
8
+ |---|---|
9
+ | Validation macro F0.5 (100,000 held-out S1, US + India) | **0.98298** (US 0.98325, India 0.98258) |
10
+ | Chosen model | stage-2 LightGBM **with** the cross-encoder + exact expected-F0.5 decision |
11
+ | **Leaderboard score** | **0.979241** (model-3: 0.976423) |
12
+ | France F0.5 implied by the leaderboard (see §2) | about **0.959** (model-3: about 0.946) |
13
+ | Submission | `output/matching_results.tsv` (95.7 MB) + `output/candidate_pairs.tsv` (151 MB) |
14
+ | Validator (`utils/validate_submission.py --check-ids`) | **PASS** — 1,732,544 rows |
15
+
16
+ Companion documents:
17
+ - `IMPLEMENTATION.md`: every stage, the features, the fail-proofing, GPU placement, and every departure
18
+ from the design.
19
+ - `DESIGN.md`: the original design this was built from.
20
+ - `Documentation_template.md`: the filled-in challenge write-up.
21
+ - `Colab/README_COLAB.md`: the standalone Google Colab copy.
22
+
23
+ ## 1. Pipeline
24
+
25
+ ```
26
+ organiser records (dataset/og/*.parquet: only entity_id, business_name, business_address, country are read)
27
+ │ prep: own normalisation of all 24.2M records from the raw text; legal forms discovered per
28
+ │ country WITHOUT labels; address parsed into segments (house number, street, locality, postcode)
29
+ ▼
30
+ splits: disjoint train S1 groups ce 120k | stack 200k | val 100k (seed 13, mixed country)
31
+ ▼
32
+ retrieval, per country, exact:
33
+ multilingual-e5-large top-100 (pre-computed vectors) bge-m3 top-100 (pre-computed vectors)
34
+ IDF lexical top-100 (name tokens, name prefix, street, locality, house+street, postcode, unit tokens)
35
+ ▼ reciprocal-rank fusion (k0=60) decides survival; cap K=64 chosen automatically
36
+ ▼ exact e5/bge cosine for every candidate + S2↔S3 "triangle" corroboration
37
+ 67 pair features ──► stage-1 LightGBM (5 grouped folds) ──► top-16 per S1 with p ≥ 0.003
38
+ ▼ (= candidate_pairs.tsv)
39
+ cross-encoder microsoft/mdeberta-v3-base, fine-tuned on the ce split ──► logit, rank, gap
40
+ ▼
41
+ stage-2 LightGBM (73 features, 5 grouped folds) ──► cross-fitted isotonic calibration
42
+ ▼
43
+ exact expected-F0.5 per S1 (Poisson-binomial DP) + one-owner arbitration to a fixpoint
44
+ ▼
45
+ output/matching_results.tsv, output/candidate_pairs.tsv, validator, per-country checks
46
+ ```
47
+
48
+ No rule depends on a country name. France goes through exactly the same code path as US and India.
49
+
50
+ ## 2. Results
51
+
52
+ **Validation** (`work/reports/report.md`, `work/models/stage2_bundle.json`). Everything was selected on the
53
+ stack split; val is used only for reporting.
54
+
55
+ | Stage | Metric | Value |
56
+ |---|---|---|
57
+ | Retrieval | fused recall at K=64 / K=100 | 98.50% / 98.78% (US 99.07%, India 97.65% at 64) |
58
+ | Retrieval | recall at 100, each retriever alone | e5 97.36%, bge 96.31%, lexical 91.06% |
59
+ | Stage 1 | AUC, stack out-of-fold / val | 0.99977 / 0.99978 |
60
+ | Stage-2 set | true pairs kept vs. blocking / vs. all true pairs | 99.92% / 98.43% |
61
+ | Cross-encoder | held-out AUC (579,705 training pairs, 4,529 steps, 1 epoch) | 0.99964 |
62
+ | Stage 2, no cross-encoder | stack F0.5: expected-F0.5 / best threshold (0.70) | 0.97144 / 0.97114 |
63
+ | Stage 2, with cross-encoder | stack F0.5: expected-F0.5 / best threshold (0.70) | **0.98248** / 0.98223 |
64
+ | **Final** | **val macro F0.5** | **0.98298** |
65
+
66
+ Predicted matches per S1 on val: US 5.73% empty and 3.32 mean; India 5.87% and 3.32. Training data has
67
+ 5.58% singletons and 3.46 true matches per S1.
68
+
69
+ Top stage-2 features by gain are the cross-encoder logit, the stage-1 probability, their gaps to the S1's
70
+ best candidate, RRF rank/score and triangle cosine (`work/reports/feature_importance.tsv`).
71
+
72
+ **Leave-one-country-out** (`work/reports/split_B.json`). This uses a stage-1-style model without the
73
+ cross-encoder, because a cross-encoder trained on both countries would leak.
74
+
75
+ | Train → validate | F0.5 |
76
+ |---|---|
77
+ | US → US | 0.973 |
78
+ | US → India | **0.854** |
79
+ | India → India | 0.965 |
80
+ | India → US | 0.934 |
81
+
82
+ **Test** (`work/reports/test_report.json`):
83
+
84
+ | Country | S1 | No match predicted | Mean matches | Top-1 neighbour shares a name word (e5 / bge) |
85
+ |---|---|---|---|---|
86
+ | France | 259,452 | 5.99% | 3.15 | 98.96% / 98.75% |
87
+ | India | 809,986 | 5.93% | 3.29 | 93.32% / 92.69% |
88
+ | US | 663,106 | 5.83% | 3.32 | 98.64% / 98.64% |
89
+
90
+ - 5,685,241 predicted pairs. 102,196 S1 are predicted singletons (5.90%). No country-shift flags.
91
+ - `candidate_pairs.tsv` holds 9,980,755 pairs (5.8 per S1). **16,407 S1 (0.95%) have no candidate**,
92
+ because stage 1 gave every candidate p < 0.003. Those S1 are predicted singletons.
93
+ - Agreement with model-3's submission (Jaccard of predicted pairs): US 0.974, India 0.971,
94
+ **France 0.917**. France is where the two models differ most.
95
+
96
+ **Leaderboard: 0.979241.** That is 0.0037 below validation.
97
+ - **Implied France score.** Assume US and India score on test what they scored on val (0.98325 / 0.98258).
98
+ Then, with the test mix (US 38.3%, India 46.8%, France 15.0%), US + India contribute 0.83570. That
99
+ leaves **France ≈ 0.959**.
100
+ - **The France estimate is sensitive.** France is only 15% of the test set, so a ±0.001 error in the
101
+ US/India estimate moves the France figure by ∓0.006.
102
+ - **Against model-3.** The same arithmetic gives model-3 about 0.946 on France (leaderboard 0.976423). So
103
+ model-4 gains about +0.0028 on the leaderboard, and most of that comes from France (+0.013 on France
104
+ ≈ +0.0019 on the leaderboard).
105
+ - **France is still the main gap.** It is about 0.024 below US/India, and it accounts for about 0.0036 of
106
+ the 0.0037 drop from validation to the leaderboard.
107
+
108
+ ## 3. Run
109
+
110
+ ```bash
111
+ cd /scratch/user4/dai/model-4
112
+ scripts/run.sh start # run / resume everything, detached (survives SSH drops)
113
+ scripts/run.sh status # per-stage state: done / pending / FAILED xN
114
+ scripts/run.sh logs # orchestrator log; scripts/run.sh tail <stage> for one stage
115
+ scripts/run.sh stop # stop (finished shards are kept)
116
+ M4_SMOKE=1 scripts/run.sh start # small end-to-end check in work_smoke/ + output_smoke/
117
+ .venv/bin/python src/run_pipeline.py --reset <stage> # force a stage and its dependents to rerun
118
+ ```
119
+
120
+ - Every stage is a separate process and writes `work/state/<stage>.done` on success, so a restart skips
121
+ finished stages.
122
+ - Retrieval, features, stage-1 scoring and cross-encoder inference checkpoint per 25k-query shard, with
123
+ atomic writes.
124
+ - Cross-encoder training checkpoints every 1,000 steps. LightGBM fold models are reused.
125
+ - On a CUDA out-of-memory error, a stage waits and retries instead of failing. Batch sizes halve
126
+ automatically.
127
+ - The scheduler puts each GPU stage on the card with the most free memory.
128
+ - Environment overrides: `M4_GPUS=0,1`, `M4_WORK`, `M4_OUTPUT`, `M4_SMOKE`. Paths and settings are in
129
+ `src/config.py`.
130
+
131
+ **Time.** The full run took about 6 h on the shared server (15:44–21:44, 2026-09-26), including
132
+ CPU-contention waits.
133
+
134
+ **Rerunning the final decision only** takes about 7 min and needs no GPU:
135
+ `src/run_pipeline.py --reset stage2_test`.
136
+
137
+ ## 4. Layout
138
+
139
+ ```
140
+ src/config.py paths (dataset/og, embeddings, validator) + all settings (CFG)
141
+ src/run_pipeline.py DAG orchestrator: retries, locking, GPU scheduling, --status/--reset/--only
142
+ src/stages.py stage registry (one subprocess per stage)
143
+ src/common.py logging, atomic saves, shard paths, progress, GPU-OOM patience
144
+ src/prep.py manifest checks, normalisation of all records, splits
145
+ src/text_utils.py normalisation, label-free legal-form discovery, address parsing, abbreviation match
146
+ src/embeddings.py pre-computed e5 / bge vectors (memory-mapped, row-order checked)
147
+ src/retrieve_dense.py exact dense top-100 per country + exact cosine / triangle features
148
+ src/retrieve_lex.py IDF sparse lexical top-100 per country
149
+ src/blocking.py RRF fusion, recall probe, automatic K
150
+ src/features.py pair features (rapidfuzz cpdist + custom)
151
+ src/stacker.py stage-1 / stage-2 LightGBM, stage-2 set, submission writing + checks, split B
152
+ src/cross_encoder.py mDeBERTa-v3 fine-tuning + multi-GPU inference
153
+ src/decide.py macro F0.5, cross-fitted isotonic, exact expected-F0.5 DP, arbitration
154
+ src/gpu_lgb.py exact GPU evaluation of LightGBM trees (bit-identical to Booster.predict)
155
+ src/stage1_test_gpu.py GPU helper that fills stage-1 test shards from the end
156
+ src/ce_infer_help.py work-stealing helper for cross-encoder inference
157
+ scripts/run.sh start | status | stop | logs | tail
158
+ utils/validate_submission.py the organisers' validator (local copy)
159
+ Colab/ standalone Hugging Face-based copy of this pipeline for Google Colab
160
+ ```
161
+
162
+ | Artefact | Size | What |
163
+ |---|---|---|
164
+ | `work/prep/` | 4.5 GB | normalised records (shared by smoke and full runs) |
165
+ | `work/models/` | 273 MB | 5 stage-1 folds, 10 stage-2 folds (with/without CE), Split B folds, `stage2_bundle.json`, `stage2_iso.pkl` |
166
+ | `work/ce/final/` | 1.1 GB | fine-tuned mDeBERTa cross-encoder |
167
+ | `work/reports/` | — | `manifest.json`, `splits.json`, `blocking_recall.json`, `s2set_train.json`, `ce_train.json`, `split_B.json`, `test_report.json`, `report.md`, `feature_importance.tsv`, `val_errors.tsv`, `nn_check_*_test.json` |
168
+ | `work/test_tsv/` | — | test TSVs written from the parquet, for the validator |
169
+ | `work/` total | 29 GB | |
170
+
171
+ The `*.fail` markers in `work/state/` (`ce_infer_test`, `eval_B`, `stage2_test`) are from earlier failed
172
+ attempts. Each of those stages later succeeded and has a `.done` marker.
173
+
174
+ ## 5. Inputs
175
+
176
+ | Input | Location |
177
+ |---|---|
178
+ | Records + ground truth | `/scratch/user4/dai/dataset/og/{train,test}_p/` (Hugging Face `Jyo-K/PopTards_Machine_Code`) |
179
+ | e5-large vectors | `/scratch/user4/dai/hybrid_bridge/prepared{,_test}/hub/e5_multilingual_large/` |
180
+ | bge-m3 vectors | `/scratch/user4/dai/bge-out/{train,test}/` |
181
+ | Python | `.venv/` (Python 3.12). It chains onto `bge-out/.venv`, which points at another user's venv under `/home/user4/Jyo/`. Pinned versions are in `requirements.txt` |
182
+
183
+ The manifest stage checks all of these: row counts, ID prefixes, duplicate IDs, and embedding row order
184
+ and unit norm. It also compares true-pair vs random-pair cosine: e5 0.923 vs 0.784, bge 0.849 vs 0.418.
185
+ The ground truth has 0 records claimed by two S1, which makes one-owner arbitration valid.
186
+
187
+ ## 6. Known limitations
188
+
189
+ 1. **France is the weak country.** The leaderboard implies about 0.959 vs about 0.983 on US/India (§2).
190
+ There are no France labels, so the causes can't be measured directly. The evidence:
191
+ - Its predicted singleton rate and match counts look like US/India.
192
+ - The leave-one-country-out loss is 3–12 points.
193
+ - France is where model-4 and model-3 disagree most (Jaccard 0.917).
194
+ 2. **The lexical index drops tokens with document frequency above 5,000 per country** (`lex_max_df`).
195
+ - That removes about 1,000 of the most frequent tokens in each country, such as common street and
196
+ legal words.
197
+ - model-3 found that this setting badly hurt its lexical channel. Here lexical recall at 100 is 91.1%
198
+ and it is one of three fused retrievers.
199
+ - The effect on fused recall has not been measured.
200
+ 3. **India retrieval is the weakest slice**: 97.65% recall at K=64 vs US 99.07%.
201
+ 4. **The cross-encoder is capped** at 600k training pairs and 1 epoch. More training is untested.
202
+ 5. **Other experiments read this folder's intermediates.**
203
+ - `/scratch/user4/dai/model-4-dual-ce/work/` symlinks `prep`, `feat`, `s2`, `ce_scores`, `splits.npz`
204
+ and `reports` from `model-4/work/`.
205
+ - Do not delete or reset those while that experiment is in use.
206
+ 6. **Shared server.** Other users share the `user4` account, both GPUs and the CPUs. The pipeline waits
207
+ rather than failing, so run times vary.
208
+
209
+ ## 7. Compliance
210
+
211
+ - **Models:**
212
+ - `intfloat/multilingual-e5-large` (MIT, 560M)
213
+ - `BAAI/bge-m3` (MIT, 568M)
214
+ - `microsoft/mdeberta-v3-base` (MIT, 278M)
215
+ - LightGBM (MIT)
216
+ - All are far below the 8B limit.
217
+ - No external data, dictionaries, APIs or geocoding. Legal forms and abbreviations are discovered from
218
+ the provided text.
219
+ - Country is used only to partition retrieval by string equality and is never a model feature.
model-4/hf_cache/.agent_harnesses.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"standardEnvVars": ["AI_AGENT", "AGENT"], "harnesses": {"antigravity": {"prettyLabel": "Antigravity", "docsUrl": "https://antigravity.google", "description": "Agentic development platform from Google built around Gemini.", "envVars": {"ANTIGRAVITY_AGENT": "*"}}, "augment-cli": {"prettyLabel": "Augment CLI", "repoUrl": "https://github.com/augmentcode/auggie", "docsUrl": "https://www.augmentcode.com", "description": "Auggie, the command-line coding agent from Augment Code.", "envVars": {"AUGMENT_AGENT": "*"}}, "cline": {"prettyLabel": "Cline", "repoUrl": "https://github.com/cline/cline", "docsUrl": "https://cline.bot", "description": "Open-source autonomous coding agent for VS Code.", "envVars": {"CLINE_ACTIVE": "*"}}, "cowork": {"prettyLabel": "Cowork", "docsUrl": "https://claude.com/product/cowork", "description": "Anthropic's agent for autonomous knowledge work, built on top of Claude Code.", "envVars": {"CLAUDE_CODE_IS_COWORK": "*"}}, "claude-code": {"prettyLabel": "Claude Code", "repoUrl": "https://github.com/anthropics/claude-code", "docsUrl": "https://code.claude.com/docs", "description": "Anthropic's agentic coding tool that lives in your terminal.", "envVars": {"CLAUDECODE": "*", "CLAUDE_CODE": "*"}}, "codex": {"prettyLabel": "Codex", "repoUrl": "https://github.com/openai/codex", "docsUrl": "https://developers.openai.com/codex", "description": "OpenAI's lightweight coding agent that runs in your terminal.", "envVars": {"CODEX_SANDBOX": "*", "CODEX_CI": "*", "CODEX_THREAD_ID": "*"}}, "crush": {"prettyLabel": "Crush", "repoUrl": "https://github.com/charmbracelet/crush", "docsUrl": "https://github.com/charmbracelet/crush", "description": "Charm's open-source AI coding agent for the terminal.", "envVars": {"CRUSH": "*"}}, "gemini-cli": {"prettyLabel": "Gemini CLI", "repoUrl": "https://github.com/google-gemini/gemini-cli", "docsUrl": "https://geminicli.com", "description": "Google's open-source terminal AI coding agent powered by Gemini models.", "envVars": {"GEMINI_CLI": "*"}}, "github-copilot": {"prettyLabel": "GitHub Copilot", "docsUrl": "https://docs.github.com/copilot", "description": "GitHub's AI coding assistant.", "envVars": {"COPILOT_MODEL": "*", "COPILOT_ALLOW_ALL": "*", "COPILOT_GITHUB_TOKEN": "*"}}, "goose": {"prettyLabel": "Goose", "repoUrl": "https://github.com/aaif-goose/goose", "docsUrl": "https://goose-docs.ai/", "description": "Open-source, extensible AI agent, originally from Block and now part of the Agentic AI Foundation.", "envVars": {"GOOSE_TERMINAL": "*"}}, "hermes-agent": {"prettyLabel": "Hermes Agent", "repoUrl": "https://github.com/NousResearch/hermes-agent", "docsUrl": "https://hermes-agent.nousresearch.com/docs", "description": "Nous Research's self-improving, multi-provider terminal AI agent.", "envVars": {"HERMES_SESSION_ID": "*"}}, "hi": {"prettyLabel": "hi", "repoUrl": "https://github.com/PipeNetwork/hi", "docsUrl": "https://github.com/PipeNetwork/hi#readme", "description": "Rust terminal coding agent with verification-in-the-loop."}, "kilo-code": {"prettyLabel": "Kilo Code", "repoUrl": "https://github.com/Kilo-Org/kilocode", "docsUrl": "https://kilocode.ai/docs", "description": "Open-source agentic coding agent for VS Code, JetBrains, and the terminal.", "envVars": {"KILOCODE_FEATURE": "*"}}, "kiro": {"prettyLabel": "Kiro", "docsUrl": "https://kiro.dev", "description": "AWS's agentic IDE for spec-driven AI software development.", "envVars": {"AGENT_CONTEXT_OUT": "*"}}, "openclaw": {"prettyLabel": "OpenClaw", "repoUrl": "https://github.com/openclaw/openclaw", "docsUrl": "https://openclaw.ai", "description": "Open-source, self-hosted personal AI assistant that runs on your own devices.", "envVars": {"OPENCLAW_SHELL": "*"}}, "sandbase-harness": {"prettyLabel": "SandBase Harness", "repoUrl": "https://github.com/sandbaseai/sandbase-harness", "docsUrl": "https://github.com/sandbaseai/sandbase-harness/blob/main/docs/installation.md", "description": "Local-first, self-hosted runtime for persistent AI agent sessions and MCP tools."}, "opencode": {"prettyLabel": "opencode", "repoUrl": "https://github.com/anomalyco/opencode", "docsUrl": "https://opencode.ai", "description": "Open-source AI coding agent built for the terminal.", "envVars": {"OPENCODE_CLIENT": "*"}}, "pi": {"prettyLabel": "Pi", "repoUrl": "https://github.com/earendil-works/pi", "docsUrl": "https://pi.dev", "description": "Minimal, self-extensible terminal coding agent with a unified multi-provider LLM API.", "envVars": {"PI_CODING_AGENT": "*"}}, "replit": {"prettyLabel": "Replit", "docsUrl": "https://replit.com", "description": "Cloud development environment with an AI coding agent.", "envVars": {"REPL_ID": "*"}}, "trae": {"prettyLabel": "Trae", "docsUrl": "https://trae.ai", "description": "AI-powered IDE from ByteDance.", "envVars": {"TRAE_AI_SHELL_ID": "*"}}, "vtcode": {"prettyLabel": "VTCode", "repoUrl": "https://github.com/vinhnx/VTCode", "description": "Rust coding agent with multi-provider LLM support, OS-native sandboxing, and extensible skills", "envVars": {"VTCODE": "1"}}, "warp": {"prettyLabel": "Warp", "repoUrl": "https://github.com/warpdotdev/Warp", "docsUrl": "https://docs.warp.dev", "description": "AI-powered terminal with an agentic Agent Mode.", "envVars": {"TERM_PROGRAM": "WarpTerminal"}}, "zed": {"prettyLabel": "Zed", "repoUrl": "https://github.com/zed-industries/zed", "docsUrl": "https://zed.dev", "description": "High-performance code editor with an integrated AI agent panel and terminal.", "envVars": {"ZED_TERM": "*"}}, "cursor-cli": {"prettyLabel": "Cursor CLI", "docsUrl": "https://cursor.com/docs/cli/overview", "description": "Cursor's coding agent for the command line.", "envVars": {"CURSOR_AGENT": "*"}}, "cursor": {"prettyLabel": "Cursor", "docsUrl": "https://cursor.com", "description": "AI-powered code editor.", "envVars": {"CURSOR_TRACE_ID": "*"}}, "devin": {"prettyLabel": "Devin", "docsUrl": "https://devin.ai", "description": "Autonomous AI software engineer from Cognition."}}}
model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3.lock ADDED
File without changes
model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/27b905d788d20826414d27ce9fa9765623e38ea7.lock ADDED
File without changes
model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959.lock ADDED
File without changes
model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead.lock ADDED
File without changes
model-4/hf_cache/hub/.locks/models--microsoft--mdeberta-v3-base/acfd94e399c5659e4bed75f91b4ee24b111fc7a6.lock ADDED
File without changes
model-4/hf_cache/hub/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by huggingface_hub.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/
model-4/hf_cache/hub/blobs/.huggingface-shared-blobs ADDED
@@ -0,0 +1 @@
 
 
1
+ 1
model-4/hf_cache/hub/blobs/7c/7ccdaee489cbb709d4624beb0f0556fc5552e6759bde1d881fca5c7a9ee5462c ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959
3
+ size 1332766994
model-4/hf_cache/hub/blobs/7c/7ccdaee489cbb709d4624beb0f0556fc5552e6759bde1d881fca5c7a9ee5462c.lock ADDED
File without changes
model-4/hf_cache/hub/blobs/7c/7ccdaee489cbb709d4624beb0f0556fc5552e6759bde1d881fca5c7a9ee5462c.refs ADDED
@@ -0,0 +1 @@
 
 
1
+ models--microsoft--mdeberta-v3-base/blobs/4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959
model-4/hf_cache/hub/blobs/99/994dbaac4eaaaf1033ce39ecedd612e77b89dbfe8efd8de25ca24bea40caf6bc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead
3
+ size 1332809049
model-4/hf_cache/hub/blobs/99/994dbaac4eaaaf1033ce39ecedd612e77b89dbfe8efd8de25ca24bea40caf6bc.lock ADDED
File without changes
model-4/hf_cache/hub/blobs/99/994dbaac4eaaaf1033ce39ecedd612e77b89dbfe8efd8de25ca24bea40caf6bc.refs ADDED
@@ -0,0 +1 @@
 
 
1
+ models--microsoft--mdeberta-v3-base/blobs/6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead
model-4/hf_cache/hub/blobs/c4/c4b914ec5ab295fbee3b0e0fe62f22febb3c09d34dd3778745e0de034a76df33 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3
3
+ size 4305025
model-4/hf_cache/hub/blobs/c4/c4b914ec5ab295fbee3b0e0fe62f22febb3c09d34dd3778745e0de034a76df33.lock ADDED
File without changes
model-4/hf_cache/hub/blobs/c4/c4b914ec5ab295fbee3b0e0fe62f22febb3c09d34dd3778745e0de034a76df33.refs ADDED
@@ -0,0 +1 @@
 
 
1
+ models--microsoft--mdeberta-v3-base/blobs/13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/added_tokens.json ADDED
File without changes
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/chat_template.jinja ADDED
File without changes
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/model.safetensors ADDED
File without changes
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/model.safetensors.index.json ADDED
File without changes
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/special_tokens_map.json ADDED
File without changes
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/.no_exist/a0484667b22365f84929a935b5e50a51f71f159d/tokenizer.json ADDED
File without changes
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3
3
+ size 4305025
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/27b905d788d20826414d27ce9fa9765623e38ea7 ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "deberta-v2",
3
+ "attention_probs_dropout_prob": 0.1,
4
+ "hidden_act": "gelu",
5
+ "hidden_dropout_prob": 0.1,
6
+ "hidden_size": 768,
7
+ "initializer_range": 0.02,
8
+ "intermediate_size": 3072,
9
+ "max_position_embeddings": 512,
10
+ "relative_attention": true,
11
+ "position_buckets": 256,
12
+ "norm_rel_ebd": "layer_norm",
13
+ "share_att_key": true,
14
+ "pos_att_type": "p2c|c2p",
15
+ "layer_norm_eps": 1e-7,
16
+ "max_relative_positions": -1,
17
+ "position_biased_input": false,
18
+ "num_attention_heads": 12,
19
+ "num_hidden_layers": 12,
20
+ "type_vocab_size": 0,
21
+ "vocab_size": 251000
22
+ }
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959
3
+ size 1332766994
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead
3
+ size 1332809049
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/blobs/acfd94e399c5659e4bed75f91b4ee24b111fc7a6 ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "do_lower_case": false,
3
+ "vocab_type": "spm"
4
+ }
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/refs/main ADDED
@@ -0,0 +1 @@
 
 
1
+ a0484667b22365f84929a935b5e50a51f71f159d
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/refs/refs/pr/5 ADDED
@@ -0,0 +1 @@
 
 
1
+ faf660b811036d7b6dfc53aacfe5281447d4e2c6
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/config.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "deberta-v2",
3
+ "attention_probs_dropout_prob": 0.1,
4
+ "hidden_act": "gelu",
5
+ "hidden_dropout_prob": 0.1,
6
+ "hidden_size": 768,
7
+ "initializer_range": 0.02,
8
+ "intermediate_size": 3072,
9
+ "max_position_embeddings": 512,
10
+ "relative_attention": true,
11
+ "position_buckets": 256,
12
+ "norm_rel_ebd": "layer_norm",
13
+ "share_att_key": true,
14
+ "pos_att_type": "p2c|c2p",
15
+ "layer_norm_eps": 1e-7,
16
+ "max_relative_positions": -1,
17
+ "position_biased_input": false,
18
+ "num_attention_heads": 12,
19
+ "num_hidden_layers": 12,
20
+ "type_vocab_size": 0,
21
+ "vocab_size": 251000
22
+ }
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f89419baf0f1aaad5cab7d53901e36a8c1af8f6b4ab58b15db9af32df656ead
3
+ size 1332809049
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/spm.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13c8d666d62a7bc4ac8f040aab68e942c861f93303156cc28f5c7e885d86d6e3
3
+ size 4305025
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/a0484667b22365f84929a935b5e50a51f71f159d/tokenizer_config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "do_lower_case": false,
3
+ "vocab_type": "spm"
4
+ }
model-4/hf_cache/hub/models--microsoft--mdeberta-v3-base/snapshots/faf660b811036d7b6dfc53aacfe5281447d4e2c6/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4809ba6a0689d9e09169f42c33531333ab5bb5eae501e26a29348ce62bea5959
3
+ size 1332766994
model-4/output/candidate_pairs.tsv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:26ec998fe938187230d01f47ce729341d9c11bd04949b91ccfb974311c7c9762
3
+ size 150987715
model-4/output/matching_results.tsv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81fd20e45c80d16fffccad360e383dff0e82e65f76e9e39646792f53035ed35d
3
+ size 95709741
model-4/output_smoke/candidate_pairs.tsv ADDED
The diff for this file is too large to render. See raw diff
 
model-4/output_smoke/matching_results.tsv ADDED
The diff for this file is too large to render. See raw diff
 
model-4/requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python 3.12, CUDA 13 (torch cu130 wheel)
2
+ anyascii==0.3.2
3
+ huggingface_hub==1.33.0
4
+ lightgbm==4.7.0
5
+ numpy==2.4.4
6
+ pandas==3.0.2
7
+ protobuf==7.36.2
8
+ pyarrow==25.0.1
9
+ RapidFuzz==3.14.6
10
+ safetensors==0.8.0
11
+ scikit-learn==1.8.0
12
+ scipy==1.18.1
13
+ sentencepiece==0.2.2
14
+ tokenizers==0.23.2
15
+ torch==2.14.0
16
+ transformers==5.17.0
model-4/scripts/run.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Usage: scripts/run.sh start|status|stop|logs|tail <stage> (prefix with M4_SMOKE=1 for the smoke run)
3
+ # The runner is detached (setsid, own process group) so it survives SSH disconnects; "start" again resumes.
4
+ set -euo pipefail
5
+ cd "$(dirname "$0")/.."
6
+ PY="$PWD/.venv/bin/python"
7
+ W=work; [ "${M4_SMOKE:-0}" = "1" ] && W=work_smoke
8
+ mkdir -p "$W/logs" "$W/state"
9
+ PIDF="$W/state/pipeline.pid"
10
+ alive() { [ -f "$PIDF" ] && kill -0 "$(cut -d' ' -f1 "$PIDF")" 2>/dev/null; }
11
+ case "${1:-}" in
12
+ start)
13
+ if alive; then echo "already running (pid $(cut -d' ' -f1 "$PIDF"))"; exit 0; fi
14
+ setsid -f "$PY" -u src/run_pipeline.py "${@:2}" >> "$W/logs/pipeline.out" 2>&1 < /dev/null
15
+ sleep 3; alive && echo "started pid $(cut -d' ' -f1 "$PIDF")" || { echo "failed to start"; tail -5 "$W/logs/pipeline.out"; } ;;
16
+ status) alive && echo "RUNNING" || echo "not running"; (cd src && "$PY" run_pipeline.py --status) ;;
17
+ stop) alive && kill -- -"$(cut -d' ' -f2 "$PIDF")" && echo stopped || echo "not running" ;;
18
+ logs) tail -n "${2:-40}" "$W/logs/pipeline.out" ;;
19
+ tail) tail -n "${3:-40}" "$W/logs/$2.log" ;;
20
+ *) echo "usage: $0 start|status|stop|logs|tail <stage>"; exit 1 ;;
21
+ esac
model-4/src/blocking.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU stage: reciprocal-rank fusion of e5 / bge / lexical candidates. RRF only decides which candidates survive
2
+ the cap; every retriever's own score/rank/gap is kept as a separate feature. Train side also runs the
3
+ recall-vs-K probe that picks the cap."""
4
+ from multiprocessing import Pool
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from common import Progress, get_logger, n_workers, save_json, save_parquet, shard_path, load_json
10
+ from config import CFG, EMB_MODELS, WORK
11
+ from decide import in_sorted
12
+ from prep import load_prep, load_splits
13
+ from retrieve_dense import n_shards, queries
14
+
15
+ log = get_logger("block")
16
+ RETR = list(EMB_MODELS) + ["lex"]
17
+ G = {}
18
+
19
+
20
+ def _load(r, split, i):
21
+ p = shard_path("lex", split, i, "npz") if r == "lex" else shard_path("dense", split, i, "npz", r)
22
+ z = np.load(p)
23
+ return z["cand"], z["score"].astype(np.float32)
24
+
25
+
26
+ def _block_shard(i):
27
+ split, Q = G["split"], G["Q"]
28
+ lo = i * CFG["query_chunk"]
29
+ frames = []
30
+ for r in RETR:
31
+ cand, score = _load(r, split, i)
32
+ nq, K = cand.shape
33
+ f = pd.DataFrame({"qi": np.repeat(np.arange(nq), K), "c": cand.ravel(),
34
+ f"{r}_r": np.tile(np.arange(K), nq), f"{r}_s": score.ravel()})
35
+ frames.append(f[f.c >= 0])
36
+ df = frames[0]
37
+ for f in frames[1:]:
38
+ df = df.merge(f, on=["qi", "c"], how="outer")
39
+ k0 = CFG["rrf_k0"]
40
+ df["rrf"] = sum((1.0 / (k0 + 1 + df[f"{r}_r"])).fillna(0) for r in RETR)
41
+ df["support"] = sum(df[f"{r}_r"].notna().astype(np.int8) for r in RETR).astype(np.int8)
42
+ df = df.sort_values(["qi", "rrf"], ascending=[True, False], kind="stable")
43
+ df["rrf_rank"] = df.groupby("qi").cumcount().astype(np.int16)
44
+ df = df[df.rrf_rank < CFG["k_max"]]
45
+ for r in RETR:
46
+ df[f"{r}_gap"] = df.groupby("qi")[f"{r}_s"].transform("max") - df[f"{r}_s"]
47
+ df[f"{r}_r"] = df[f"{r}_r"].fillna(CFG["dense_k"]).astype(np.int16)
48
+ df["q"] = Q[lo + df.qi.values].astype(np.int32)
49
+ df["c"] = df.c.astype(np.int32)
50
+ if split == "train":
51
+ key = df.q.values.astype(np.int64) * G["mult"] + df.c.values
52
+ df["y"] = in_sorted(key, G["gt_key"]).astype(np.int8)
53
+ else:
54
+ df["y"] = np.int8(-1)
55
+ cols = ["q", "c", "rrf", "rrf_rank", "support", "y"] + [f"{r}_{x}" for r in RETR for x in ("r", "s", "gap")]
56
+ for c in cols:
57
+ if df[c].dtype == np.float64:
58
+ df[c] = df[c].astype(np.float32)
59
+ save_parquet(df[cols].reset_index(drop=True), shard_path("block", split, i))
60
+ return i
61
+
62
+
63
+ def run_block(split):
64
+ ns = n_shards(split)
65
+ Q = queries(split)
66
+ G.update(split=split, Q=Q)
67
+ if split == "train":
68
+ sp = load_splits()
69
+ mult = np.int64(1 << 32)
70
+ G["mult"] = mult
71
+ G["gt_key"] = np.sort(sp["gt_q"] * mult + sp["gt_c"])
72
+ todo = [i for i in range(ns) if not shard_path("block", split, i).exists()]
73
+ if todo:
74
+ prog = Progress(log, len(todo), f"block {split} shards")
75
+ with Pool(min(n_workers(), 16)) as pool:
76
+ for _ in pool.imap_unordered(_block_shard, todo):
77
+ prog.step()
78
+ if split == "train":
79
+ recall_probe(ns)
80
+
81
+
82
+ def recall_probe(ns):
83
+ sp = load_splits()
84
+ n_true = sp["n_true"]
85
+ evalq = np.concatenate([sp["g_stack"], sp["g_val"]])
86
+ country = load_prep("train", "s1", ["country"]).country.values
87
+ df = pd.concat([pd.read_parquet(shard_path("block", "train", i),
88
+ columns=["q", "rrf_rank", "y"] + [f"{r}_r" for r in RETR]) for i in range(ns)])
89
+ df = df[np.isin(df.q.values, evalq) & (df.y == 1)]
90
+ tot = int(n_true[evalq].sum())
91
+ ctry = pd.Series(country[evalq]).value_counts()
92
+ tot_c = pd.Series(n_true[evalq]).groupby(country[evalq]).sum()
93
+ rep = {"true_pairs": tot, "recall_at_K": {}, "recall_at_K_by_country": {}, "per_retriever_recall@100": {}}
94
+ for K in CFG["k_grid"]:
95
+ m = df.rrf_rank < K
96
+ rep["recall_at_K"][K] = float(m.sum() / tot)
97
+ rep["recall_at_K_by_country"][K] = (df[m].groupby(country[df[m].q.values]).size() / tot_c).round(4).to_dict()
98
+ for r in RETR:
99
+ rep["per_retriever_recall@100"][r] = float((df[f"{r}_r"] < CFG["dense_k"]).sum() / tot)
100
+ full = rep["recall_at_K"][CFG["k_max"]]
101
+ K = next(k for k in CFG["k_grid"] if rep["recall_at_K"][k] >= CFG["k_recall_frac"] * full)
102
+ rep["chosen_K"] = K
103
+ save_json(rep, WORK / "reports" / "blocking_recall.json")
104
+ save_json({"K": int(K)}, WORK / "reports" / "k_choice.json")
105
+ log.info(f"blocking recall: {rep}")
model-4/src/ce_infer_help.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Work-stealing helper for cross-encoder inference: scores the shards of another rank from the END backwards
2
+ (the owner goes forwards; shards are written atomically and both skip finished ones, so they meet).
3
+
4
+ CUDA_VISIBLE_DEVICES=<fast gpu> python ce_infer_help.py <split> <rank_to_help> <world>
5
+ """
6
+ import sys
7
+
8
+ from common import shard_path
9
+ from cross_encoder import CE, _n_ce_shards, _score, _texts, _tok_model, gpu_patient, log
10
+
11
+ import pandas as pd
12
+ import torch
13
+ from config import CFG, WORK
14
+
15
+
16
+ def main():
17
+ split, rank, world = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
18
+ AutoTokenizer, AutoModel = _tok_model()
19
+ n, ns = _n_ce_shards(split)
20
+ s2 = pd.read_parquet(WORK / "s2" / f"{split}.parquet", columns=["q", "c"])
21
+ ta, tb = _texts(split, "s1"), _texts(split, "pool")
22
+ tok = AutoTokenizer.from_pretrained(CE / "final")
23
+ model = AutoModel.from_pretrained(CE / "final", dtype=torch.float32)
24
+ model = gpu_patient(lambda: model.cuda().eval(), log, "load cross-encoder")
25
+ from common import save_npy
26
+ for i in range(ns - 1, -1, -1):
27
+ if i % world != rank or shard_path("ce_scores", split, i, "npy").exists():
28
+ continue
29
+ sl = slice(i * CFG["ce_shard_pairs"], (i + 1) * CFG["ce_shard_pairs"])
30
+ q, c = s2.q.values[sl], s2.c.values[sl]
31
+ scores = _score(model, tok, ta[q], tb[c])
32
+ if not shard_path("ce_scores", split, i, "npy").exists():
33
+ save_npy(shard_path("ce_scores", split, i, "npy"), scores)
34
+ log.info(f"helper scored shard {i}")
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()