add Cold Start tab: SSL pretraining lifts new-merchant fraud catch ~5x
Browse filesnew encoder/src/demo/cold_start.py renders the result tab: three stat
cards, the two-panel figure (fraud caught vs label history; fraud
caught vs approval rate at 1% labels), a 7-column operating-point
table read from docs/figures/recall_at_approval.json, the "how it
works" paragraph for the SSL stage, and a synthetic-data scope block.
wired as a new tab in both app.py (standalone 4-tab) and
copilot_app_unified.py (7-tab unified). figure is base64-embedded so
the tab is self-contained html. *.png added to LFS tracking.
result on synthetic data (held-out test, n=20k, 3.7% base rate, 1%
labels = stand-in for a newly onboarded entity, 95% approval rate):
fraud caught 7.5% -> 36.0% (~4.8x), false-decline rate 4.9% -> 3.8%.
roc-auc 0.575 -> 0.814; pr-auc 0.048 -> 0.210. whole-book bonus at
full labels and 1% decline budget: +16 pts.
mechanism: stage-1 self-supervised next-feature prediction through the
frozen lfm2.5 base anchors the encoder's value tables before the
supervised fine-tune. same connector-pretraining stage lfm2.5-audio
and lfm2.5-vl already use.
- .gitattributes +1 -0
- README.md +6 -1
- docs/figures/cold_start_demo.png +3 -0
- docs/figures/recall_at_approval.json +211 -0
- encoder/src/demo/app.py +8 -0
- encoder/src/demo/cold_start.py +346 -0
- encoder/src/demo/copilot_app_unified.py +8 -5
|
@@ -1,3 +1,4 @@
|
|
| 1 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.npz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 1 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
|
@@ -22,11 +22,16 @@ per-surface LoRA adapts the attention layers.
|
|
| 22 |
|
| 23 |
## What this demo shows
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
- **Multi-Head Demo** — the original 4-task-head encoder demo. Live
|
| 28 |
inference on curated customer archetypes: fraud probability,
|
| 29 |
next-merchant prediction, amount-bucket prediction, MCC prediction.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
- **Why Liquid** — architectural pitch for the encoder-on-frozen-backbone
|
| 31 |
recipe.
|
| 32 |
- **Integration** — build-it-yourself guide.
|
|
|
|
| 22 |
|
| 23 |
## What this demo shows
|
| 24 |
|
| 25 |
+
Seven tabs over one shared LFM2.5-350M backbone:
|
| 26 |
|
| 27 |
- **Multi-Head Demo** — the original 4-task-head encoder demo. Live
|
| 28 |
inference on curated customer archetypes: fraud probability,
|
| 29 |
next-merchant prediction, amount-bucket prediction, MCC prediction.
|
| 30 |
+
- **Cold Start** — a brief self-supervised pretraining stage ("SSL")
|
| 31 |
+
catches ~5× more fraud at a fixed approval rate on a newly onboarded
|
| 32 |
+
merchant with almost no labels, while declining fewer good customers.
|
| 33 |
+
The same connector-pretraining stage LFM2.5-Audio and LFM2.5-VL use.
|
| 34 |
+
Synthetic-data demonstration with operating-point numbers + figure.
|
| 35 |
- **Why Liquid** — architectural pitch for the encoder-on-frozen-backbone
|
| 36 |
recipe.
|
| 37 |
- **Integration** — build-it-yourself guide.
|
|
Git LFS Details
|
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_rate": 0.03665,
|
| 3 |
+
"n_test": 20000,
|
| 4 |
+
"split": "test",
|
| 5 |
+
"runs": {
|
| 6 |
+
"ssl_1pct": {
|
| 7 |
+
"roc_auc": 0.8140344300750755,
|
| 8 |
+
"pr_auc": 0.2104627331942034,
|
| 9 |
+
"operating_points": {
|
| 10 |
+
"appr99": {
|
| 11 |
+
"approval_rate": 0.99,
|
| 12 |
+
"decline_rate": 0.01145,
|
| 13 |
+
"recall": 0.12551159618008187,
|
| 14 |
+
"false_decline_rate": 0.007110603622774692,
|
| 15 |
+
"precision": 0.4017467248908297
|
| 16 |
+
},
|
| 17 |
+
"appr98": {
|
| 18 |
+
"approval_rate": 0.98,
|
| 19 |
+
"decline_rate": 0.02005,
|
| 20 |
+
"recall": 0.19508867667121418,
|
| 21 |
+
"false_decline_rate": 0.01339077178595526,
|
| 22 |
+
"precision": 0.3566084788029925
|
| 23 |
+
},
|
| 24 |
+
"appr97": {
|
| 25 |
+
"approval_rate": 0.97,
|
| 26 |
+
"decline_rate": 0.03005,
|
| 27 |
+
"recall": 0.2660300136425648,
|
| 28 |
+
"false_decline_rate": 0.021072299787200915,
|
| 29 |
+
"precision": 0.324459234608985
|
| 30 |
+
},
|
| 31 |
+
"appr95": {
|
| 32 |
+
"approval_rate": 0.95,
|
| 33 |
+
"decline_rate": 0.05005,
|
| 34 |
+
"recall": 0.3601637107776262,
|
| 35 |
+
"false_decline_rate": 0.03825193335755437,
|
| 36 |
+
"precision": 0.26373626373626374
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"nossl_1pct": {
|
| 41 |
+
"roc_auc": 0.5746099668824207,
|
| 42 |
+
"pr_auc": 0.04786141744299679,
|
| 43 |
+
"operating_points": {
|
| 44 |
+
"appr99": {
|
| 45 |
+
"approval_rate": 0.99,
|
| 46 |
+
"decline_rate": 0.01,
|
| 47 |
+
"recall": 0.021828103683492497,
|
| 48 |
+
"false_decline_rate": 0.009550007785332434,
|
| 49 |
+
"precision": 0.08
|
| 50 |
+
},
|
| 51 |
+
"appr98": {
|
| 52 |
+
"approval_rate": 0.98,
|
| 53 |
+
"decline_rate": 0.02005,
|
| 54 |
+
"recall": 0.03956343792633015,
|
| 55 |
+
"false_decline_rate": 0.0193076244355634,
|
| 56 |
+
"precision": 0.07231920199501247
|
| 57 |
+
},
|
| 58 |
+
"appr97": {
|
| 59 |
+
"approval_rate": 0.97,
|
| 60 |
+
"decline_rate": 0.03,
|
| 61 |
+
"recall": 0.05320600272851296,
|
| 62 |
+
"false_decline_rate": 0.029117143302018995,
|
| 63 |
+
"precision": 0.065
|
| 64 |
+
},
|
| 65 |
+
"appr95": {
|
| 66 |
+
"approval_rate": 0.95,
|
| 67 |
+
"decline_rate": 0.0502,
|
| 68 |
+
"recall": 0.07503410641200546,
|
| 69 |
+
"false_decline_rate": 0.04925520319717652,
|
| 70 |
+
"precision": 0.054780876494023904
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"ssl_10pct": {
|
| 75 |
+
"roc_auc": 0.9393715555037555,
|
| 76 |
+
"pr_auc": 0.8570152888812671,
|
| 77 |
+
"operating_points": {
|
| 78 |
+
"appr99": {
|
| 79 |
+
"approval_rate": 0.99,
|
| 80 |
+
"decline_rate": 0.02215,
|
| 81 |
+
"recall": 0.6016371077762619,
|
| 82 |
+
"false_decline_rate": 0.00010380443244926559,
|
| 83 |
+
"precision": 0.9954853273137697
|
| 84 |
+
},
|
| 85 |
+
"appr98": {
|
| 86 |
+
"approval_rate": 0.98,
|
| 87 |
+
"decline_rate": 0.02215,
|
| 88 |
+
"recall": 0.6016371077762619,
|
| 89 |
+
"false_decline_rate": 0.00010380443244926559,
|
| 90 |
+
"precision": 0.9954853273137697
|
| 91 |
+
},
|
| 92 |
+
"appr97": {
|
| 93 |
+
"approval_rate": 0.97,
|
| 94 |
+
"decline_rate": 0.03,
|
| 95 |
+
"recall": 0.7803547066848567,
|
| 96 |
+
"false_decline_rate": 0.0014532620542897182,
|
| 97 |
+
"precision": 0.9533333333333334
|
| 98 |
+
},
|
| 99 |
+
"appr95": {
|
| 100 |
+
"approval_rate": 0.95,
|
| 101 |
+
"decline_rate": 0.05015,
|
| 102 |
+
"recall": 0.8472032742155525,
|
| 103 |
+
"false_decline_rate": 0.019826646597809725,
|
| 104 |
+
"precision": 0.6191425722831505
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
"nossl_10pct": {
|
| 109 |
+
"roc_auc": 0.9259714016664364,
|
| 110 |
+
"pr_auc": 0.7596305449815982,
|
| 111 |
+
"operating_points": {
|
| 112 |
+
"appr99": {
|
| 113 |
+
"approval_rate": 0.99,
|
| 114 |
+
"decline_rate": 0.0139,
|
| 115 |
+
"recall": 0.37789904502046384,
|
| 116 |
+
"false_decline_rate": 5.1902216224632795e-05,
|
| 117 |
+
"precision": 0.9964028776978417
|
| 118 |
+
},
|
| 119 |
+
"appr98": {
|
| 120 |
+
"approval_rate": 0.98,
|
| 121 |
+
"decline_rate": 0.02,
|
| 122 |
+
"recall": 0.5266030013642565,
|
| 123 |
+
"false_decline_rate": 0.0007266310271448591,
|
| 124 |
+
"precision": 0.965
|
| 125 |
+
},
|
| 126 |
+
"appr97": {
|
| 127 |
+
"approval_rate": 0.97,
|
| 128 |
+
"decline_rate": 0.03,
|
| 129 |
+
"recall": 0.6657571623465212,
|
| 130 |
+
"false_decline_rate": 0.005813048217158873,
|
| 131 |
+
"precision": 0.8133333333333334
|
| 132 |
+
},
|
| 133 |
+
"appr95": {
|
| 134 |
+
"approval_rate": 0.95,
|
| 135 |
+
"decline_rate": 0.05,
|
| 136 |
+
"recall": 0.7612551159618008,
|
| 137 |
+
"false_decline_rate": 0.022940779571287696,
|
| 138 |
+
"precision": 0.558
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"ssl_100pct": {
|
| 143 |
+
"roc_auc": 0.9561845809915676,
|
| 144 |
+
"pr_auc": 0.8820119418197224,
|
| 145 |
+
"operating_points": {
|
| 146 |
+
"appr99": {
|
| 147 |
+
"approval_rate": 0.99,
|
| 148 |
+
"decline_rate": 0.01595,
|
| 149 |
+
"recall": 0.43519781718963163,
|
| 150 |
+
"false_decline_rate": 0.0,
|
| 151 |
+
"precision": 1.0
|
| 152 |
+
},
|
| 153 |
+
"appr98": {
|
| 154 |
+
"approval_rate": 0.98,
|
| 155 |
+
"decline_rate": 0.02015,
|
| 156 |
+
"recall": 0.5497953615279673,
|
| 157 |
+
"false_decline_rate": 0.0,
|
| 158 |
+
"precision": 1.0
|
| 159 |
+
},
|
| 160 |
+
"appr97": {
|
| 161 |
+
"approval_rate": 0.97,
|
| 162 |
+
"decline_rate": 0.03005,
|
| 163 |
+
"recall": 0.7939972714870396,
|
| 164 |
+
"false_decline_rate": 0.000986142108268023,
|
| 165 |
+
"precision": 0.9683860232945092
|
| 166 |
+
},
|
| 167 |
+
"appr95": {
|
| 168 |
+
"approval_rate": 0.95,
|
| 169 |
+
"decline_rate": 0.05,
|
| 170 |
+
"recall": 0.8663028649386084,
|
| 171 |
+
"false_decline_rate": 0.01894430892199097,
|
| 172 |
+
"precision": 0.635
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
"nossl_100pct": {
|
| 177 |
+
"roc_auc": 0.9452378866918683,
|
| 178 |
+
"pr_auc": 0.7709143923552446,
|
| 179 |
+
"operating_points": {
|
| 180 |
+
"appr99": {
|
| 181 |
+
"approval_rate": 0.99,
|
| 182 |
+
"decline_rate": 0.01005,
|
| 183 |
+
"recall": 0.2742155525238745,
|
| 184 |
+
"false_decline_rate": 0.0,
|
| 185 |
+
"precision": 1.0
|
| 186 |
+
},
|
| 187 |
+
"appr98": {
|
| 188 |
+
"approval_rate": 0.98,
|
| 189 |
+
"decline_rate": 0.02,
|
| 190 |
+
"recall": 0.5252387448840382,
|
| 191 |
+
"false_decline_rate": 0.0007785332433694919,
|
| 192 |
+
"precision": 0.9625
|
| 193 |
+
},
|
| 194 |
+
"appr97": {
|
| 195 |
+
"approval_rate": 0.97,
|
| 196 |
+
"decline_rate": 0.03035,
|
| 197 |
+
"recall": 0.669849931787176,
|
| 198 |
+
"false_decline_rate": 0.006020657082057404,
|
| 199 |
+
"precision": 0.8088962108731467
|
| 200 |
+
},
|
| 201 |
+
"appr95": {
|
| 202 |
+
"approval_rate": 0.95,
|
| 203 |
+
"decline_rate": 0.05005,
|
| 204 |
+
"recall": 0.7639836289222374,
|
| 205 |
+
"false_decline_rate": 0.02288887735506306,
|
| 206 |
+
"precision": 0.5594405594405595
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
|
@@ -36,6 +36,7 @@ from src.demo.decode import TransactionDecoder
|
|
| 36 |
from src.demo.merchant_catalog import DemoMerchantCatalog
|
| 37 |
from src.demo.profile_inference import format_profile_html, infer_profile
|
| 38 |
|
|
|
|
| 39 |
from encoder.src.demo.inference import EncoderDemoModel
|
| 40 |
from encoder.src.demo.render import (
|
| 41 |
format_amount_predictions,
|
|
@@ -380,6 +381,11 @@ def _build_integration_tab_contents() -> None:
|
|
| 380 |
gr.HTML(render_encoder_integration())
|
| 381 |
|
| 382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
def create_app(
|
| 384 |
model: EncoderDemoModel,
|
| 385 |
data: DemoData,
|
|
@@ -396,6 +402,8 @@ def create_app(
|
|
| 396 |
with gr.Tabs():
|
| 397 |
with gr.Tab("Demo"):
|
| 398 |
_build_demo_tab_contents(model, data, decoder, merchant_catalog, app)
|
|
|
|
|
|
|
| 399 |
with gr.Tab("Why Liquid"):
|
| 400 |
_build_why_liquid_tab_contents()
|
| 401 |
with gr.Tab("Integration"):
|
|
|
|
| 36 |
from src.demo.merchant_catalog import DemoMerchantCatalog
|
| 37 |
from src.demo.profile_inference import format_profile_html, infer_profile
|
| 38 |
|
| 39 |
+
from encoder.src.demo.cold_start import render_cold_start
|
| 40 |
from encoder.src.demo.inference import EncoderDemoModel
|
| 41 |
from encoder.src.demo.render import (
|
| 42 |
format_amount_predictions,
|
|
|
|
| 381 |
gr.HTML(render_encoder_integration())
|
| 382 |
|
| 383 |
|
| 384 |
+
def _build_cold_start_tab_contents() -> None:
|
| 385 |
+
"""Render the Cold Start tab content (HTML self-supervised-pretraining result)."""
|
| 386 |
+
gr.HTML(render_cold_start())
|
| 387 |
+
|
| 388 |
+
|
| 389 |
def create_app(
|
| 390 |
model: EncoderDemoModel,
|
| 391 |
data: DemoData,
|
|
|
|
| 402 |
with gr.Tabs():
|
| 403 |
with gr.Tab("Demo"):
|
| 404 |
_build_demo_tab_contents(model, data, decoder, merchant_catalog, app)
|
| 405 |
+
with gr.Tab("Cold Start"):
|
| 406 |
+
_build_cold_start_tab_contents()
|
| 407 |
with gr.Tab("Why Liquid"):
|
| 408 |
_build_why_liquid_tab_contents()
|
| 409 |
with gr.Tab("Integration"):
|
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cold-start (self-supervised pretraining) result tab for the encoder demo.
|
| 2 |
+
|
| 3 |
+
A third piece of demo-specific content alongside `render_why_encoder` and
|
| 4 |
+
`render_encoder_integration` in render.py. It presents one result: on a newly
|
| 5 |
+
onboarded merchant with almost no fraud labels, adding a brief self-supervised
|
| 6 |
+
pretraining stage ("SSL") catches far more fraud at the same approval rate
|
| 7 |
+
while declining fewer good customers. The lift concentrates where label history
|
| 8 |
+
is scarce (new merchants, new markets) and shrinks as history accumulates.
|
| 9 |
+
|
| 10 |
+
Design choices, and why:
|
| 11 |
+
- Numbers are read at render time from the recall-at-approval JSON produced by
|
| 12 |
+
`encoder/scripts/recall_at_approval.py`. That artifact is the single source
|
| 13 |
+
of truth; nothing here is hardcoded, so the tab can never drift from the
|
| 14 |
+
measured numbers.
|
| 15 |
+
- The figure is base64-embedded as a data URI so the tab is self-contained
|
| 16 |
+
HTML, matching the other content tabs (no Gradio static-file-serving config).
|
| 17 |
+
- Design tokens are imported from render.py so there is one design system.
|
| 18 |
+
|
| 19 |
+
Scope of claim: every number is synthetic-data, held-out test split. The tab
|
| 20 |
+
labels it a demonstration throughout. It is never a production or customer
|
| 21 |
+
guarantee; the pilot produces the number on the customer's portfolio.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import base64
|
| 27 |
+
import json
|
| 28 |
+
import logging
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
from typing import Any
|
| 31 |
+
|
| 32 |
+
# Single source of truth for the design system lives in render.py. Importing the
|
| 33 |
+
# tokens (rather than redefining them) keeps this tab visually identical to the
|
| 34 |
+
# Why Liquid / Integration tabs.
|
| 35 |
+
from encoder.src.demo.render import (
|
| 36 |
+
_BG_CARD,
|
| 37 |
+
_BG_CARD_ALT,
|
| 38 |
+
_BORDER,
|
| 39 |
+
_BORDER_SUBTLE,
|
| 40 |
+
_CONTAINER_WIDTH,
|
| 41 |
+
_FONT_MONO,
|
| 42 |
+
_RADIUS_CARD,
|
| 43 |
+
_TEXT,
|
| 44 |
+
_TEXT_DIM,
|
| 45 |
+
_TEXT_MUTED,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
log = logging.getLogger(__name__)
|
| 49 |
+
|
| 50 |
+
# Repo-root-relative defaults (robust to the process cwd). This file lives at
|
| 51 |
+
# encoder/src/demo/, so parents[3] is the repository root.
|
| 52 |
+
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
| 53 |
+
# Neutral figure (no customer name in caption) for the generic Gradio demo.
|
| 54 |
+
# The dLocal-specific figure (docs/figures/dlocal_cold_start.png) is used in
|
| 55 |
+
# the dLocal deck only; pass figure_path explicitly to switch.
|
| 56 |
+
_FIG_DEFAULT = _REPO_ROOT / "docs" / "figures" / "cold_start_demo.png"
|
| 57 |
+
_JSON_DEFAULT = _REPO_ROOT / "docs" / "figures" / "recall_at_approval.json"
|
| 58 |
+
|
| 59 |
+
# Teal = "with SSL" in the figure; gray = "without". Reuse the same mapping for
|
| 60 |
+
# the table so the tab's color language matches the embedded chart.
|
| 61 |
+
_SSL = "#0F766E" # teal-700, with SSL
|
| 62 |
+
_SSL_BG = "rgba(15,118,110,0.06)"
|
| 63 |
+
_NO = "#6B7280" # gray-500, without SSL
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ---------------------------------------------------------------------------
|
| 67 |
+
# Small formatters and JSON accessors
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _img_data_uri(path: Path) -> str:
|
| 72 |
+
"""Base64-encode a PNG as an inline data URI. Returns '' if the file is absent.
|
| 73 |
+
|
| 74 |
+
The figure is cosmetic; a missing file degrades to the table-only view with
|
| 75 |
+
a logged warning rather than crashing the app.
|
| 76 |
+
"""
|
| 77 |
+
try:
|
| 78 |
+
b64 = base64.b64encode(path.read_bytes()).decode("ascii")
|
| 79 |
+
except OSError as exc:
|
| 80 |
+
log.warning("cold-start figure not found at %s (%s); rendering without it", path, exc)
|
| 81 |
+
return ""
|
| 82 |
+
return f"data:image/png;base64,{b64}"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _op(runs: Any, tag: str, appr: int, field: str) -> float: # noqa: ANN401 (json is Any)
|
| 86 |
+
"""One operating-point field (recall / false_decline_rate / precision) from the JSON."""
|
| 87 |
+
return float(runs[tag]["operating_points"][f"appr{appr}"][field])
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _pct(x: float) -> str:
|
| 91 |
+
"""Recall / percentage with one decimal, e.g. 0.3601 -> '36.0%'."""
|
| 92 |
+
return f"{x * 100:.1f}%"
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _fdr(x: float) -> str:
|
| 96 |
+
"""False-decline rate; small values keep two decimals so they don't read as zero."""
|
| 97 |
+
p = x * 100
|
| 98 |
+
if p >= 1.0:
|
| 99 |
+
return f"{p:.1f}%"
|
| 100 |
+
if p > 0.0:
|
| 101 |
+
return f"{p:.2f}%"
|
| 102 |
+
return "0.0%"
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _lift(ssl: float, base: float) -> str:
|
| 106 |
+
"""Multiplicative lift as 'N.N×'.
|
| 107 |
+
|
| 108 |
+
Computed from the displayed (1-decimal-percent) recall values, not the raw
|
| 109 |
+
fractions, so the lift column equals SSL% / no-SSL% when a reader divides the
|
| 110 |
+
two columns by eye (and matches the talk track). Guarded against divide-by-zero.
|
| 111 |
+
"""
|
| 112 |
+
s = round(ssl * 100, 1)
|
| 113 |
+
b = round(base * 100, 1)
|
| 114 |
+
return f"{s / max(b, 1e-9):.1f}×"
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# ---------------------------------------------------------------------------
|
| 118 |
+
# HTML fragments
|
| 119 |
+
# ---------------------------------------------------------------------------
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _stat_card(big: str, big_color: str, label: str, detail: str) -> str:
|
| 123 |
+
"""One headline stat: a large colored number, a label, and a supporting line."""
|
| 124 |
+
return f"""
|
| 125 |
+
<div style="padding: 14px 16px; background: {_BG_CARD_ALT}; border: 1px solid {_BORDER};
|
| 126 |
+
border-radius: {_RADIUS_CARD};">
|
| 127 |
+
<div style="font-size: 28px; font-weight: 700; color: {big_color};
|
| 128 |
+
letter-spacing: -0.02em; line-height: 1;">{big}</div>
|
| 129 |
+
<div style="font-size: 12px; font-weight: 600; color: {_TEXT}; margin: 8px 0 4px 0;">
|
| 130 |
+
{label}</div>
|
| 131 |
+
<div style="font-size: 11px; color: {_TEXT_DIM}; line-height: 1.45;">{detail}</div>
|
| 132 |
+
</div>"""
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _table_header(cols: list[str]) -> str:
|
| 136 |
+
ths = ""
|
| 137 |
+
for i, c in enumerate(cols):
|
| 138 |
+
align = "left" if i == 0 else "right"
|
| 139 |
+
ths += (
|
| 140 |
+
f'<th style="padding: 6px 10px; font-size: 10px; color: {_TEXT_DIM};'
|
| 141 |
+
f' text-transform: uppercase; letter-spacing: 0.05em; text-align: {align};'
|
| 142 |
+
f' font-weight: 600;">{c}</th>'
|
| 143 |
+
)
|
| 144 |
+
return f"<tr style='border-bottom: 1px solid {_BORDER};'>{ths}</tr>"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _table_row(cells: list[str], highlight: bool = False) -> str:
|
| 148 |
+
bg = f"background: {_SSL_BG};" if highlight else ""
|
| 149 |
+
tds = ""
|
| 150 |
+
for i, c in enumerate(cells):
|
| 151 |
+
align = "left" if i == 0 else "right"
|
| 152 |
+
weight = "600" if highlight and i == 0 else "400"
|
| 153 |
+
tds += (
|
| 154 |
+
f'<td style="padding: 6px 10px; font-family: {_FONT_MONO}; font-size: 11px;'
|
| 155 |
+
f' color: {_TEXT}; text-align: {align}; font-weight: {weight};">{c}</td>'
|
| 156 |
+
)
|
| 157 |
+
return f"<tr style='border-bottom: 1px solid {_BORDER_SUBTLE}; {bg}'>{tds}</tr>"
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# ---------------------------------------------------------------------------
|
| 161 |
+
# Main render
|
| 162 |
+
# ---------------------------------------------------------------------------
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def render_cold_start(
|
| 166 |
+
json_path: Path | None = None,
|
| 167 |
+
figure_path: Path | None = None,
|
| 168 |
+
) -> str:
|
| 169 |
+
"""Render the Cold Start (SSL result) tab content as an HTML string.
|
| 170 |
+
|
| 171 |
+
Reads every number from the recall-at-approval JSON (single source of truth)
|
| 172 |
+
and base64-embeds the result figure so the tab is self-contained.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
json_path: recall_at_approval.json. Defaults to docs/figures/ in the repo.
|
| 176 |
+
figure_path: dlocal_cold_start.png. Defaults to docs/figures/ in the repo.
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
HTML string suitable for a ``gr.HTML`` block.
|
| 180 |
+
"""
|
| 181 |
+
json_path = json_path or _JSON_DEFAULT
|
| 182 |
+
figure_path = figure_path or _FIG_DEFAULT
|
| 183 |
+
|
| 184 |
+
raw: Any = json.loads(json_path.read_text())
|
| 185 |
+
runs: Any = raw["runs"]
|
| 186 |
+
n_test = int(raw["n_test"])
|
| 187 |
+
base_rate = float(raw["base_rate"])
|
| 188 |
+
|
| 189 |
+
# Headline (cold start = 1% labels, 95% approval).
|
| 190 |
+
cs_ssl = _op(runs, "ssl_1pct", 95, "recall")
|
| 191 |
+
cs_no = _op(runs, "nossl_1pct", 95, "recall")
|
| 192 |
+
cs_fdr_ssl = _op(runs, "ssl_1pct", 95, "false_decline_rate")
|
| 193 |
+
cs_fdr_no = _op(runs, "nossl_1pct", 95, "false_decline_rate")
|
| 194 |
+
# Whole-book bonus at full labels, tight 1% decline budget.
|
| 195 |
+
wb_ssl = _op(runs, "ssl_100pct", 99, "recall")
|
| 196 |
+
wb_no = _op(runs, "nossl_100pct", 99, "recall")
|
| 197 |
+
wb_delta_pts = (wb_ssl - wb_no) * 100
|
| 198 |
+
|
| 199 |
+
stats = "".join([
|
| 200 |
+
_stat_card(
|
| 201 |
+
_lift(cs_ssl, cs_no), _SSL,
|
| 202 |
+
"more fraud caught on a new merchant",
|
| 203 |
+
f"At a 95% approval rate with only 1% of labels: {_pct(cs_ssl)} of fraud "
|
| 204 |
+
f"caught vs {_pct(cs_no)} without pretraining.",
|
| 205 |
+
),
|
| 206 |
+
_stat_card(
|
| 207 |
+
f"−{(cs_fdr_no - cs_fdr_ssl) * 100:.1f} pts", _SSL,
|
| 208 |
+
"fewer good customers declined",
|
| 209 |
+
f"False-decline rate {_fdr(cs_fdr_ssl)} vs {_fdr(cs_fdr_no)} at the same "
|
| 210 |
+
f"approval rate. More fraud caught and fewer false declines, together.",
|
| 211 |
+
),
|
| 212 |
+
_stat_card(
|
| 213 |
+
f"+{wb_delta_pts:.0f} pts", _TEXT,
|
| 214 |
+
"whole-book bonus at full labels",
|
| 215 |
+
f"Even with full label history, {_pct(wb_ssl)} vs {_pct(wb_no)} fraud caught "
|
| 216 |
+
"at a tight 1% decline budget. Not only a new-entity trick.",
|
| 217 |
+
),
|
| 218 |
+
])
|
| 219 |
+
|
| 220 |
+
# Business-units table. (label, frac_tag, approval, highlight-as-cold-start)
|
| 221 |
+
specs: list[tuple[str, str, int, bool]] = [
|
| 222 |
+
("1% — new entity", "1pct", 99, True),
|
| 223 |
+
("1% — new entity", "1pct", 95, True),
|
| 224 |
+
("10%", "10pct", 99, False),
|
| 225 |
+
("100% — full", "100pct", 99, False),
|
| 226 |
+
("100% — full", "100pct", 95, False),
|
| 227 |
+
]
|
| 228 |
+
rows = ""
|
| 229 |
+
for label, frac, appr, hl in specs:
|
| 230 |
+
ssl_r = _op(runs, f"ssl_{frac}", appr, "recall")
|
| 231 |
+
no_r = _op(runs, f"nossl_{frac}", appr, "recall")
|
| 232 |
+
ssl_f = _op(runs, f"ssl_{frac}", appr, "false_decline_rate")
|
| 233 |
+
no_f = _op(runs, f"nossl_{frac}", appr, "false_decline_rate")
|
| 234 |
+
rows += _table_row(
|
| 235 |
+
[label, f"{appr}%", _pct(ssl_r), _pct(no_r), _lift(ssl_r, no_r),
|
| 236 |
+
_fdr(ssl_f), _fdr(no_f)],
|
| 237 |
+
highlight=hl,
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# Ranking quality, for the evaluator who wants AUC alongside operating points.
|
| 241 |
+
roc_1 = (float(runs["nossl_1pct"]["roc_auc"]), float(runs["ssl_1pct"]["roc_auc"]))
|
| 242 |
+
roc_100 = (float(runs["nossl_100pct"]["roc_auc"]), float(runs["ssl_100pct"]["roc_auc"]))
|
| 243 |
+
pr_1 = (float(runs["nossl_1pct"]["pr_auc"]), float(runs["ssl_1pct"]["pr_auc"]))
|
| 244 |
+
pr_100 = (float(runs["nossl_100pct"]["pr_auc"]), float(runs["ssl_100pct"]["pr_auc"]))
|
| 245 |
+
|
| 246 |
+
fig_uri = _img_data_uri(figure_path)
|
| 247 |
+
if fig_uri:
|
| 248 |
+
figure_block = f"""
|
| 249 |
+
<div style="padding: 12px; background: {_BG_CARD}; border: 1px solid {_BORDER};
|
| 250 |
+
border-radius: {_RADIUS_CARD}; margin-bottom: 16px; text-align: center;">
|
| 251 |
+
<img src="{fig_uri}" alt="Cold-start fraud-catch lift from self-supervised pretraining"
|
| 252 |
+
style="max-width: 100%; height: auto; border-radius: 8px;" />
|
| 253 |
+
</div>"""
|
| 254 |
+
else:
|
| 255 |
+
figure_block = ""
|
| 256 |
+
|
| 257 |
+
return f"""
|
| 258 |
+
<div style="max-width: {_CONTAINER_WIDTH}; margin: 0 auto; padding: 16px;
|
| 259 |
+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;">
|
| 260 |
+
|
| 261 |
+
<!-- Lead -->
|
| 262 |
+
<h2 style="margin: 0 0 4px 0; color: {_TEXT}; font-size: 22px; font-weight: 700;
|
| 263 |
+
letter-spacing: -0.02em;">
|
| 264 |
+
Cold Start: the Fraud-Catch Lift Where You Have No History Yet
|
| 265 |
+
</h2>
|
| 266 |
+
<p style="color: {_TEXT_DIM}; font-size: 13px; margin: 0 0 20px 0; line-height: 1.55;">
|
| 267 |
+
A newly onboarded merchant or market has almost no fraud labels, so a model trained
|
| 268 |
+
only on labels has nothing to stand on. Adding one brief self-supervised pretraining
|
| 269 |
+
stage — before any labels — anchors the model on transaction <i>behavior</i>
|
| 270 |
+
learned across the rest of the book. The result: it catches far more fraud the day you
|
| 271 |
+
enter a new market, at the same approval rate, while turning away fewer good customers.
|
| 272 |
+
</p>
|
| 273 |
+
|
| 274 |
+
<!-- Headline stats -->
|
| 275 |
+
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 16px;">
|
| 276 |
+
{stats}
|
| 277 |
+
</div>
|
| 278 |
+
|
| 279 |
+
<!-- Figure -->
|
| 280 |
+
{figure_block}
|
| 281 |
+
|
| 282 |
+
<!-- Operating-point table -->
|
| 283 |
+
<div style="padding: 16px 20px; background: {_BG_CARD}; border: 1px solid {_BORDER};
|
| 284 |
+
border-radius: {_RADIUS_CARD}; margin-bottom: 16px;">
|
| 285 |
+
<h3 style="color: {_TEXT}; margin: 0 0 4px 0; font-size: 15px; font-weight: 600;">
|
| 286 |
+
Fraud Caught at a Fixed Approval Rate
|
| 287 |
+
</h3>
|
| 288 |
+
<p style="color: {_TEXT_MUTED}; font-size: 12px; line-height: 1.55; margin: 0 0 10px 0;">
|
| 289 |
+
What a fraud team actually runs: hold the approval rate fixed (decline the riskiest
|
| 290 |
+
tail), then measure how much fraud you catch and how many good customers you decline.
|
| 291 |
+
Label history stands in for entity age — <b>1% = a new entity</b>, 100% = a
|
| 292 |
+
mature one. The lift is largest where history is scarce and shrinks as it accumulates.
|
| 293 |
+
</p>
|
| 294 |
+
<table style="width: 100%; border-collapse: collapse; margin-bottom: 8px;">
|
| 295 |
+
{_table_header(["Label history", "Approval", "Fraud caught (SSL)",
|
| 296 |
+
"Fraud caught (no SSL)", "Lift", "False decl. (SSL)", "(no SSL)"])}
|
| 297 |
+
{rows}
|
| 298 |
+
</table>
|
| 299 |
+
<p style="font-size: 11px; color: {_TEXT_DIM}; margin: 0; line-height: 1.5;">
|
| 300 |
+
Ranking quality (test split): ROC-AUC {roc_1[0]:.3f} → <b>{roc_1[1]:.3f}</b> at 1%
|
| 301 |
+
labels, {roc_100[0]:.3f} → <b>{roc_100[1]:.3f}</b> at full labels.
|
| 302 |
+
PR-AUC {pr_1[0]:.3f} → <b>{pr_1[1]:.3f}</b> and
|
| 303 |
+
{pr_100[0]:.3f} → <b>{pr_100[1]:.3f}</b> — PR-AUC is the honest metric at a
|
| 304 |
+
{base_rate:.1%} fraud base rate.
|
| 305 |
+
</p>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
<!-- How it works -->
|
| 309 |
+
<div style="padding: 16px 20px; background: {_BG_CARD}; border: 1px solid {_BORDER};
|
| 310 |
+
border-radius: {_RADIUS_CARD}; margin-bottom: 16px;">
|
| 311 |
+
<h3 style="color: {_TEXT}; margin: 0 0 4px 0; font-size: 15px; font-weight: 600;">
|
| 312 |
+
How It Works: One Added Pretraining Stage
|
| 313 |
+
</h3>
|
| 314 |
+
<p style="color: {_TEXT_MUTED}; font-size: 13px; line-height: 1.6; margin: 0;">
|
| 315 |
+
The encoder originally trained in a single supervised pass with randomly-initialized
|
| 316 |
+
per-feature embeddings. We add a stage-1 self-supervised step — next-feature
|
| 317 |
+
prediction over the transaction corpus, run through the frozen LFM2.5 base — that
|
| 318 |
+
anchors the encoder’s value tables <i>before</i> the supervised fine-tune. It is
|
| 319 |
+
the same connector-pretraining stage LFM2.5-Audio and LFM2.5-VL already use, which the
|
| 320 |
+
transaction encoder had skipped. The base stays frozen and the shared-base,
|
| 321 |
+
many-surfaces story is unchanged; this adds an unsupervised warm-up, not a new model.
|
| 322 |
+
</p>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
<!-- Scope / caveat -->
|
| 326 |
+
<div style="padding: 14px 18px; background: {_BG_CARD_ALT}; border: 1px solid {_BORDER};
|
| 327 |
+
border-radius: {_RADIUS_CARD}; margin-bottom: 16px;">
|
| 328 |
+
<div style="font-family: {_FONT_MONO}; font-size: 10px; color: {_TEXT_DIM};
|
| 329 |
+
font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
|
| 330 |
+
margin-bottom: 6px;">Scope of claim</div>
|
| 331 |
+
<p style="color: {_TEXT_MUTED}; font-size: 12px; line-height: 1.6; margin: 0;">
|
| 332 |
+
Synthetic data, held-out test split (n={n_test:,}, fraud base rate {base_rate:.1%}).
|
| 333 |
+
Label-scarcity (1% ≈ 1,700 labeled sequences) stands in for a newly onboarded
|
| 334 |
+
entity before label history accumulates. <b>The absolute numbers don’t
|
| 335 |
+
transfer; the shape and the mechanism do.</b> The pilot produces the real number on
|
| 336 |
+
the customer’s portfolio — hold out a recently onboarded market, score fraud
|
| 337 |
+
on it cold against the current baseline, at the customer’s approval rate.
|
| 338 |
+
</p>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
<div style="font-family: {_FONT_MONO}; font-size: 10px; color: {_TEXT_DIM}; text-align: center;">
|
| 342 |
+
Same architecture, same frozen base, same fine-tune — trained with vs without the
|
| 343 |
+
self-supervised stage. One variable changed.
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
"""
|
|
@@ -42,6 +42,7 @@ from src.demo.decode import TransactionDecoder
|
|
| 42 |
from src.demo.merchant_catalog import DemoMerchantCatalog
|
| 43 |
|
| 44 |
from encoder.src.demo.app import (
|
|
|
|
| 45 |
_build_demo_tab_contents,
|
| 46 |
_build_integration_tab_contents,
|
| 47 |
_build_why_liquid_tab_contents,
|
|
@@ -94,6 +95,8 @@ def build_unified_ui(
|
|
| 94 |
multihead_merchant_catalog,
|
| 95 |
app,
|
| 96 |
)
|
|
|
|
|
|
|
| 97 |
with gr.Tab("Why Liquid"):
|
| 98 |
_build_why_liquid_tab_contents()
|
| 99 |
with gr.Tab("Integration"):
|
|
@@ -116,12 +119,12 @@ def _load_multihead(
|
|
| 116 |
device: str,
|
| 117 |
) -> tuple[EncoderDemoModel, DemoData, TransactionDecoder, DemoMerchantCatalog]:
|
| 118 |
"""Load the original multi-head V3 model + curated test data."""
|
| 119 |
-
print(
|
| 120 |
schema_cfg = load_schema(schema)
|
| 121 |
data = DemoData(data_dir, schema_cfg)
|
| 122 |
merchant_catalog = DemoMerchantCatalog(schema_cfg)
|
| 123 |
decoder = TransactionDecoder(schema_cfg, merchant_catalog)
|
| 124 |
-
print(
|
| 125 |
model = EncoderDemoModel(
|
| 126 |
model_config_path=config,
|
| 127 |
schema_path=schema,
|
|
@@ -148,7 +151,7 @@ def _load_copilots(
|
|
| 148 |
device: torch.device,
|
| 149 |
) -> tuple[CopilotModel, CollectionsCopilotModel, FraudPatternCopilotModel]:
|
| 150 |
"""Load the three Co-Pilot surfaces. Each has its own backbone copy."""
|
| 151 |
-
print(
|
| 152 |
dispute_model = CopilotModel.from_paths(
|
| 153 |
checkpoint_path=dispute_checkpoint,
|
| 154 |
model_config_path=dispute_config,
|
|
@@ -157,7 +160,7 @@ def _load_copilots(
|
|
| 157 |
cast_path=dispute_cast,
|
| 158 |
device=device,
|
| 159 |
)
|
| 160 |
-
print(
|
| 161 |
collections_model = CollectionsCopilotModel.from_paths(
|
| 162 |
checkpoint_path=collections_checkpoint,
|
| 163 |
model_config_path=collections_config,
|
|
@@ -166,7 +169,7 @@ def _load_copilots(
|
|
| 166 |
cast_path=collections_cast,
|
| 167 |
device=device,
|
| 168 |
)
|
| 169 |
-
print(
|
| 170 |
fraud_model = FraudPatternCopilotModel.from_paths(
|
| 171 |
checkpoint_path=fraud_checkpoint,
|
| 172 |
model_config_path=fraud_config,
|
|
|
|
| 42 |
from src.demo.merchant_catalog import DemoMerchantCatalog
|
| 43 |
|
| 44 |
from encoder.src.demo.app import (
|
| 45 |
+
_build_cold_start_tab_contents,
|
| 46 |
_build_demo_tab_contents,
|
| 47 |
_build_integration_tab_contents,
|
| 48 |
_build_why_liquid_tab_contents,
|
|
|
|
| 95 |
multihead_merchant_catalog,
|
| 96 |
app,
|
| 97 |
)
|
| 98 |
+
with gr.Tab("Cold Start"):
|
| 99 |
+
_build_cold_start_tab_contents()
|
| 100 |
with gr.Tab("Why Liquid"):
|
| 101 |
_build_why_liquid_tab_contents()
|
| 102 |
with gr.Tab("Integration"):
|
|
|
|
| 119 |
device: str,
|
| 120 |
) -> tuple[EncoderDemoModel, DemoData, TransactionDecoder, DemoMerchantCatalog]:
|
| 121 |
"""Load the original multi-head V3 model + curated test data."""
|
| 122 |
+
print("[multihead] schema + data ...")
|
| 123 |
schema_cfg = load_schema(schema)
|
| 124 |
data = DemoData(data_dir, schema_cfg)
|
| 125 |
merchant_catalog = DemoMerchantCatalog(schema_cfg)
|
| 126 |
decoder = TransactionDecoder(schema_cfg, merchant_catalog)
|
| 127 |
+
print("[multihead] loading EncoderDemoModel ...")
|
| 128 |
model = EncoderDemoModel(
|
| 129 |
model_config_path=config,
|
| 130 |
schema_path=schema,
|
|
|
|
| 151 |
device: torch.device,
|
| 152 |
) -> tuple[CopilotModel, CollectionsCopilotModel, FraudPatternCopilotModel]:
|
| 153 |
"""Load the three Co-Pilot surfaces. Each has its own backbone copy."""
|
| 154 |
+
print("[copilot 1/3] loading Dispute ...")
|
| 155 |
dispute_model = CopilotModel.from_paths(
|
| 156 |
checkpoint_path=dispute_checkpoint,
|
| 157 |
model_config_path=dispute_config,
|
|
|
|
| 160 |
cast_path=dispute_cast,
|
| 161 |
device=device,
|
| 162 |
)
|
| 163 |
+
print("[copilot 2/3] loading Collections ...")
|
| 164 |
collections_model = CollectionsCopilotModel.from_paths(
|
| 165 |
checkpoint_path=collections_checkpoint,
|
| 166 |
model_config_path=collections_config,
|
|
|
|
| 169 |
cast_path=collections_cast,
|
| 170 |
device=device,
|
| 171 |
)
|
| 172 |
+
print("[copilot 3/3] loading Fraud ...")
|
| 173 |
fraud_model = FraudPatternCopilotModel.from_paths(
|
| 174 |
checkpoint_path=fraud_checkpoint,
|
| 175 |
model_config_path=fraud_config,
|