Tirthanker commited on
Commit
34bb407
Β·
verified Β·
1 Parent(s): 8c0583f

README: engine v2.0 is the model to use; older models moved to archive/

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. README.md +328 -344
  2. archive/README.md +11 -0
  3. {legacy_v1_1 β†’ archive/engine_v1_1}/backend_modes_report.json +0 -0
  4. {legacy_v1_1 β†’ archive/engine_v1_1}/build_report.json +0 -0
  5. {legacy_v1_1 β†’ archive/engine_v1_1}/cold_stage2.onnx +0 -0
  6. {legacy_v1_1 β†’ archive/engine_v1_1}/meta.json +0 -0
  7. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed0.pth +0 -0
  8. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed1.pth +0 -0
  9. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed2.pth +0 -0
  10. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed3.pth +0 -0
  11. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed4.pth +0 -0
  12. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed0.pth +0 -0
  13. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed1.pth +0 -0
  14. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed2.pth +0 -0
  15. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed3.pth +0 -0
  16. {legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed4.pth +0 -0
  17. {legacy_v1_1 β†’ archive/engine_v1_1}/sensitivity_report.json +0 -0
  18. {legacy_v1_1 β†’ archive/engine_v1_1}/validation_v1_1.json +0 -0
  19. {legacy_v1_1 β†’ archive/engine_v1_1}/verification_report.json +0 -0
  20. {legacy_v1_1 β†’ archive/engine_v1_1}/warm_deepfm.onnx +0 -0
  21. {phase1_models β†’ archive/phase1_models}/README.md +0 -0
  22. {phase1_models β†’ archive/phase1_models}/docs/Model_Explainer.docx +0 -0
  23. {phase1_models β†’ archive/phase1_models}/example_inference.py +0 -0
  24. {phase1_models β†’ archive/phase1_models}/metadata/ctr_vocab.json +0 -0
  25. {phase1_models β†’ archive/phase1_models}/metadata/export_report.json +0 -0
  26. {phase1_models β†’ archive/phase1_models}/metadata/feature_stats.json +0 -0
  27. {phase1_models β†’ archive/phase1_models}/metadata/pca.json +0 -0
  28. {phase1_models β†’ archive/phase1_models}/metadata/sasrec_vocab.json +0 -0
  29. {phase1_models β†’ archive/phase1_models}/onnx/DCNV2.onnx +0 -0
  30. {phase1_models β†’ archive/phase1_models}/onnx/DeepFM.onnx +0 -0
  31. {phase1_models β†’ archive/phase1_models}/onnx/LR.onnx +0 -0
  32. {phase1_models β†’ archive/phase1_models}/onnx/SASRec.onnx +0 -0
  33. {phase1_models β†’ archive/phase1_models}/onnx/WideDeep.onnx +0 -0
  34. {phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/DCNV2.pth +0 -0
  35. {phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/DeepFM.pth +0 -0
  36. {phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/LR.pth +0 -0
  37. {phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/SASRec.pth +0 -0
  38. {phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/WideDeep.pth +0 -0
  39. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/engine_features.json +0 -0
  40. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/results_real_backtest.json +0 -0
  41. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/results_simulation.json +0 -0
  42. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/results_tag_quality.json +0 -0
  43. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage1.json +0 -0
  44. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_DeepFM_meta.json +0 -0
  45. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_LR_meta.json +0 -0
  46. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_WideDeep_meta.json +0 -0
  47. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_summary.json +0 -0
  48. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/onnx/DeepFM.onnx +0 -0
  49. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/onnx/LR.onnx +0 -0
  50. {v1_two_stage_engine β†’ archive/v1_two_stage_engine}/onnx/WideDeep.onnx +0 -0
README.md CHANGED
@@ -1,344 +1,328 @@
1
- ---
2
- license: apache-2.0
3
- library_name: onnx
4
- language:
5
- - en
6
- pipeline_tag: tabular-classification
7
- tags:
8
- - recommender-system
9
- - feed-ranking
10
- - ctr-prediction
11
- - cold-start
12
- - deepfm
13
- - wide-and-deep
14
- - onnx
15
- - recbole
16
- ---
17
-
18
- # UniHive RecSys: hybrid feed-ranking engine (v2.0)
19
-
20
- A small, production-shaped **post recommendation engine** for the UniHive student social app. It ranks candidate posts for a user and handles the full lifecycle of a user and a post:
21
-
22
- - **Brand-new users** are ranked by **v2b**: an engagement model (Wide&Deep) plus a small bonus for closeness between the post's tags and the user's picked interests.
23
- - **As a user builds up engaged history**, the engine **gradually blends in a warm DeepFM model** that reads their behaviour. It always keeps at least 30% v2b for freshness.
24
- - **New posts** get a **cold-start push** that fades as they collect impressions. **Older posts** sink through a **gentle freshness decay**.
25
- - **Topic-first ordering:** in every block of 10 positions, 7 go to posts matching the user's interests.
26
-
27
- Everything runs on CPU through ONNX Runtime: about **0.7 ms per candidate**, about 2 MB of models.
28
-
29
- > **Honest scope.** The models were trained on a small real-world trace: 4 users, 1,794 Reddit post exposures, 44 positive test examples. The engine consistently beats random and popularity, but trails Reddit's own feed order. Treat the defaults as a well-tested starting point to re-tune on your own logs. All numbers below come from held-out or later-recorded data.
30
-
31
- ---
32
-
33
- ## Contents
34
- - [How it works](#how-it-works)
35
- - [Quick start](#quick-start)
36
- - [Inputs and outputs](#inputs-and-outputs)
37
- - [Using it inside a backend (UniHive adapter)](#using-it-inside-a-backend-unihive-adapter)
38
- - [All scores](#all-scores)
39
- - [Where the engine's parts come from (industry lineage)](#where-the-engines-parts-come-from-industry-lineage)
40
- - [Model sizes (why the files are small)](#model-sizes-why-the-files-are-small)
41
- - [Files in this repo](#files-in-this-repo)
42
- - [Training data and procedure](#training-data-and-procedure)
43
- - [Limitations and responsible use](#limitations-and-responsible-use)
44
- - [Citation](#citation)
45
-
46
- ---
47
-
48
- ## How it works
49
-
50
- ```
51
- candidate posts (already retrieved by your backend)
52
- β”‚
53
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
54
- β–Ό β–Ό β–Ό
55
- tag closeness COLD model (Wide&Deep) WARM model (DeepFM)
56
- 8 tag-match features post stats + content post inputs + 27 user-history
57
- β†’ logistic score + tag-match features features (no IDs, no feed position)
58
- β”‚ β†’ Platt-calibrated P(engage) β†’ P(engage)
59
- └──────────► v2b = P_cold + 0.1 Β· rank(closeness) β”‚
60
- β”‚ β”‚
61
- blend = (1 βˆ’ w) Β· rank(v2b) + w Β· rank(warm) w = min(0.7, engaged / (engaged + 10))
62
- β”‚
63
- score = blend Β· freshness(age) + push(age, impressions)
64
- β”‚
65
- topic-first order: each block of 10 = 7 best topic matches + 3 best of any kind
66
- ```
67
-
68
- | Piece | Formula | Default |
69
- |---|---|---|
70
- | Warm weight | `w = 0` with no engaged posts, else `min(w_max, e / (e + n0))` | n0 = 10, w_max = 0.7 β†’ 0.09 at 1 engaged post, 0.33 at 5, 0.50 at 10, 0.67 at 20 |
71
- | Freshness | `floor + (1 βˆ’ floor) Β· 0.5^(age_h / half_life)` | half-life 720 h, floor 0.85 (a week-old post keeps 98%, a month-old 93%) |
72
- | Cold-start push | `beta Β· [age_h ≀ 24] / sqrt(1 + impressions)` | beta 0.1 |
73
- | Topic-first | 7 of every 10 positions go to posts sharing a picked tag or its broad domain | `topic_first_slots = 7` |
74
-
75
- **What the models do NOT use.** User or post IDs, feed position, scroll speed, community (subreddit) name and flair are all excluded. The content vectors are built from title + body + image caption only. A built-in test fails if community or flair ever becomes an input again.
76
-
77
- ---
78
-
79
- ## Quick start
80
-
81
- ```bash
82
- pip install huggingface_hub numpy onnxruntime
83
- hf download Tirthanker/UniHive_RecSys --local-dir UniHive_RecSys
84
- cd UniHive_RecSys
85
- python -m hybrid_engine.example # demo: a brand-new user vs the same user after 30 engaged posts
86
- python -m hybrid_engine.tests.test_engine # smoke tests + the no-community guard
87
- ```
88
-
89
- ```python
90
- from hybrid_engine import HybridRecommender
91
-
92
- engine = HybridRecommender() # loads hybrid_engine/artifacts once (CPU, ONNX)
93
-
94
- user = {
95
- "interests": ["machine-learning", "computer-vision"], # tag slugs the user picked (114-tag taxonomy, see meta.json)
96
- "history": [ # past interactions; anything not before now_ms is ignored
97
- {"post_id": "p1", "ts_ms": 1791141039157, "visible_ms": 5200, "focal_ms": 4800, "hover_ms": 300,
98
- "clicked": True, "engaged": True, "community": "c-ml", "author": "a-17", "embedding": None},
99
- ],
100
- }
101
- posts = [
102
- {"post_id": "p9", "post_type": "text", "score": 42, "comments": 7, "age_hours": 3.5,
103
- "title": "Fine-tuning ViTs on a laptop GPU", "body": "...", "tags": {"computer-vision": 0.9, "deep-learning": 0.7},
104
- "embedding": None, # optional Qwen3-Embedding-0.6B vector (1024 floats); None = content unknown
105
- "author": "a-3", "impressions": 0, "community": "c-cv"},
106
- # ... more candidates
107
- ]
108
-
109
- feed = engine.recommend(user, posts, now_ms=1791145000000, k=10)
110
- for d in feed:
111
- print(d["position"], d["post_id"], round(d["score"], 3), "warm weight", d["warm_weight"])
112
- ```
113
-
114
- **Content vectors (recommended).** Embed `"title. body"` (+ `" Image: <caption>"` when there is one) with `Qwen/Qwen3-Embedding-0.6B`, `normalize_embeddings=True`. `hybrid_engine.backend_adapter.qwen3_text()` builds the exact training text. Without vectors the engine still works; it simply loses the content signal (see the scores).
115
-
116
- **Knobs.** `HybridRecommender(w_max=0.5, n0_engaged_posts=5, topic_first_slots=0, push_beta=0, decay_floor=1.0, ...)`. The defaults are in `artifacts/meta.json`.
117
-
118
- ---
119
-
120
- ## Inputs and outputs
121
-
122
- **Candidate post fields:**
123
- - `post_id`, `post_type` (text / image / video / link / ...), `score` (likes), `comments`, `age_hours`, `title`, `body`, `image_caption`;
124
- - `tags` (`{slug: confidence}` or a list), `embedding` (1024 floats or None), `author`, `impressions` (how often your platform has shown it);
125
- - optional: `domain`, `language`, `is_ad`, `is_spoiler`, `upvote_ratio`, `awards`, `is_member`;
126
- - `community` is used only for the user's *own* per-community engagement history, never as a model input.
127
-
128
- **History item fields:** `post_id`, `ts_ms` (when shown), `visible_ms`, `focal_ms`, `hover_ms`, `clicked`, `engaged` (dwell β‰₯ 3 s or click / like / save / join / reply), `community`, `author`, `embedding`.
129
-
130
- **Output per candidate:**
131
- - `score`, `position`, `topic_match`;
132
- - `v2b`, `cold_probability`, `tag_closeness`, `warm_probability`, `warm_weight`, `engaged_history`;
133
- - `freshness_multiplier`, `push`, `has_content_embedding`.
134
-
135
- Log these to explain every feed.
136
-
137
- ---
138
-
139
- ## Using it inside a backend (UniHive adapter)
140
-
141
- `hybrid_engine/backend_adapter.py` connects the engine to a FastAPI + Redis + Postgres feed service:
142
- - **`post_from_meta` / `post_from_row`:** backend post rows β†’ candidates. Post types are mapped, the link domain is parsed and age is computed.
143
- - **`history_from_events`:** raw client events (VIEW, LONG_VIEW, CLICK, LIKE, SAVE, SKIP, HIDE, ...) β†’ interactions. Events on one post within 30 min count as one visit, an explicit "not interested" overrides dwell, and the shown time is corrected for when the event arrived.
144
- - **`expand_interests`:** broad interest picks β†’ child tags.
145
- - **`build_pool_hybrid`:** a drop-in replacement for a `build_pool(r, uid, state, level)` scoring step. The backend's own retrieval is kept; only the ranking changes.
146
- - **`encode_q3` / `fetch_q3` / `q3_key`:** store and read float16 Qwen3 vectors in Redis.
147
-
148
- Step-by-step integration, latency and rollout advice are in [`hybrid_engine/INTEGRATION.md`](hybrid_engine/INTEGRATION.md). The adapter tests can also run against the real backend store on fakeredis: `UNIHIVE_BACKEND=/path/to/backend python -m hybrid_engine.tests.test_backend_adapter`.
149
-
150
- ---
151
-
152
- ## All scores
153
-
154
- Metrics:
155
- - **AUC:** chance that an engaged post is scored above a non-engaged one (0.5 = random).
156
- - **NDCG@10:** quality of the top 10, where 1.0 means every engaged post comes first.
157
- - **25-post pages:** a feed ranked one page at a time, with history updated as the user scrolls.
158
-
159
- ### 1. Engine models (test split: last 15% of each user's timeline, 270 exposures, 44 engaged)
160
- | Model | Validation AUC | Test AUC | Notes |
161
- |---|---|---|---|
162
- | Cold model (Wide&Deep, 5-seed ensemble) | 0.777 | **0.602** | post + tag-match features only; works for a brand-new user |
163
- | Warm model (DeepFM, 5-seed ensemble) | 0.713 | **0.565** | per-seed test AUC 0.55–0.60 |
164
- | v1.1 cold model (used community + flair) | 0.799 | 0.692 | legacy, see below |
165
- | v1.1 warm model (used community + flair) | β€” | 0.671 | legacy |
166
-
167
- The AUC standard error with 44 positives is about 0.046.
168
-
169
- ### 2. Engineering checks
170
- | Check | Result |
171
- |---|---|
172
- | ONNX vs PyTorch, max difference | 2.4e-7 |
173
- | Engine-built features vs training files (539 rows) | max 0.0021 standardized units; 0 categorical mismatches |
174
- | Engine scores vs offline predictions | cold 1.1e-4, warm 2.5e-4 |
175
- | Latency | 0.7 ms per candidate on 1 CPU thread, including history features |
176
- | Automated tests | 9 pass (smoke, no-community guard, 7 backend-adapter tests including the real backend store on fakeredis) |
177
-
178
- ### 3. Ranking quality, offline periods and later sessions (NDCG@10)
179
- Each feed is ranked as one live request. "Backend data" adds the limits of a typical app backend: no flair, tags without confidences, a single dwell value, no Reddit vote ratio.
180
-
181
- | Feed | v2.0 | v2.0, backend data | v2.0, backend data, no content vectors | Random |
182
- |---|---|---|---|---|
183
- | Validation period, whole feeds | **0.650** | 0.579 | 0.604 | 0.350 |
184
- | Validation period, 25-post pages | 0.559 | 0.595 | 0.582 | 0.350 |
185
- | Test period, whole feeds | **0.340** | 0.329 | 0.268 | 0.201 |
186
- | Test period, 25-post pages | 0.415 | 0.397 | 0.441 | 0.286 |
187
- | Later session 1, one feed | 0.255 | 0.159 | 0.155 | 0.119 |
188
- | Later session 1, 25-post pages | 0.468 | 0.486 | 0.362 | 0.282 |
189
- | Later session 2, one feed | 0.110 | 0.110 | 0.110 | 0.079 |
190
- | Later session 2, 25-post pages | 0.403 | 0.382 | 0.372 | 0.245 |
191
- | **Mean** | **0.400** | **0.380** | **0.362** | **0.239** |
192
-
193
- ### 4. The two later real sessions in detail (recorded two days after all training data)
194
- | | Session 1: 89 posts, 11 engaged | | | Session 2: 142 posts, 10 engaged | | |
195
- |---|---|---|---|---|---|---|
196
- | | NDCG@10 | engaged in top 10 | AUC | NDCG@10 | engaged in top 10 | AUC |
197
- | **Engine v2.0** | 0.255 | 3 | 0.688 | 0.110 | 1 | 0.498 |
198
- | v2.0 without content vectors | 0.249 | 3 | 0.760 | 0.110 | 1 | 0.532 |
199
- | Engine v1.1 (with community) | 0.413 | 5 | 0.780 | 0.110 | 1 | 0.602 |
200
- | Reddit's own feed order | 0.641 | 6 | 0.917 | 0.312 | 3 | 0.773 |
201
- | Random | 0.124 | 1.1 | 0.50 | 0.070 | 0.6 | 0.50 |
202
-
203
- On 25-post pages, v2.0 scored 0.468 and 0.403 (v1.1: 0.559 and 0.381).
204
-
205
- ### 5. Why v2.0 removed community and flair, and what it cost
206
- Community names and flairs are specific to the source platform: a new platform's communities are unknown to a model trained on Reddit. Removing them costs offline accuracy on Reddit data. Cold-model diagnosis, 5 seeds:
207
-
208
- | Variant | Test AUC |
209
- |---|---|
210
- | With community + flair inputs and in the embedding text (v1.1) | 0.692 |
211
- | Inputs removed only | 0.624 |
212
- | Embedding text changed only | 0.653 |
213
- | Both removed (v2.0) | 0.602 |
214
-
215
- Under conditions a new platform would actually have (unknown community names, backend data), the mean NDCG@10 gap shrinks: **0.380 for v2.0 vs 0.410 for v1.1**, and 0.362 vs 0.369 without content vectors. The v1.1 figures are optimistic, because its content vectors still contain Reddit community names.
216
-
217
- ### 6. Earlier research results that shaped the design (test period, NDCG@10)
218
- | Approach | NDCG@10 |
219
- |---|---|
220
- | Reddit feed order (reference, uses information a recommender cannot have) | 0.659 |
221
- | DeepFM with history + feed position | 0.491 |
222
- | **v2b (model + 0.1 Γ— tag closeness): the engine's new-user score** | **0.467** |
223
- | 10% exploration slots | 0.447 |
224
- | Stage-2 model alone | 0.402 |
225
- | Two-tower retrieval + model ranking | 0.390 |
226
- | First two-stage engine (tag order + probability filter) | 0.291 |
227
- | X-style Thunder + Phoenix pipeline, full | 0.258 |
228
- | Popularity | 0.328 |
229
- | Learned next-intent embedding | 0.180 |
230
- | Random | 0.192 |
231
-
232
- Full benchmark tables (calibration, slices, ablations, significance tests, the random-interest simulation) are in [`REPORT.md`](REPORT.md).
233
-
234
- ---
235
-
236
- ## Where the engine's parts come from (industry lineage)
237
-
238
- The engine is assembled from techniques that run in widely used production recommenders. Each part below is an established method we implemented or evaluated. This repo is an independent implementation, not affiliated with these companies.
239
-
240
- | Part of this engine | Technique | Where it is used in industry |
241
- |---|---|---|
242
- | **Cold model** | **Wide & Deep** (Cheng et al., 2016) | Introduced by Google for **Google Play** app recommendations, where it was deployed in production |
243
- | **Warm model** | **DeepFM** (Guo et al., 2017) | Developed by Huawei Noah's Ark Lab and evaluated on the **Huawei App Market**; a standard CTR model across ads and app stores |
244
- | Phase-1 benchmark model | **DCN V2** (Wang et al., 2021) | Google's deep & cross network, reported in production at **Google** for ranking |
245
- | Phase-1 next-post model | **SASRec** (Kang & McAuley, 2018) | Self-attentive sequential recommendation; the basis of many "next item" systems |
246
- | **Hybrid cold β†’ warm blend** | content/popularity model for new users, behavioural model as history grows | The standard cold-start pattern in e-commerce and streaming feeds |
247
- | **Probability calibration** | **Platt scaling** | Common for CTR models in ads systems, where calibrated probabilities feed auctions |
248
- | **Freshness decay** | exponential time decay of a post's score | The same family as the time-decayed ranking of **Hacker News** and **Reddit "hot"** |
249
- | **Cold-start push** | boost for new items that fades with exposure | Used by feeds such as **TikTok** to give every new video an initial audience, and in bandit-style new-item exploration |
250
- | **Topic-first slots + exploration share** | slot quotas, with 15–20% exploration kept in the serving layer | Quota-based feed blending and exploration as used in large feeds (**YouTube**, **Instagram**) |
251
- | **Content vectors** | **Qwen3-Embedding-0.6B** (Alibaba Qwen team) | A widely used open embedding model for search and retrieval |
252
- | **Interest tags** | LLM tagging with a closed taxonomy (gpt-oss-120b on Groq) | LLM-generated item labels are increasingly used to cold-start catalogs |
253
- | Evaluated, not shipped | **Two-tower retrieval** (Yi et al., 2019) | **YouTube**'s candidate retrieval and Google's recommendation stacks |
254
- | Evaluated, not shipped | **Thunder + Phoenix** pipeline | Modelled on **X (Twitter)**'s open-sourced recommendation algorithm: in-network candidate source + transformer ranker |
255
- | Evaluated, not shipped | **MMR diversification** (Carbonell & Goldstein, 1998) | Classic re-ranking for result diversity in search and feeds |
256
- | Training framework | **RecBole** | Open-source library from Renmin University of China, used to train all five phase-1 models |
257
-
258
- ---
259
-
260
- ## Model sizes (why the files are small)
261
-
262
- The trained models are deliberately small: they learn from about 1,800 rows, and a larger network would only memorise them. Parameter counts below are read from the checkpoints.
263
-
264
- | Model | Parameters | One copy (float32) | Shipped as |
265
- |---|---|---|---|
266
- | **Engine v2.0 cold model** (Wide&Deep: 4-wide embeddings β†’ MLP 64 β†’ 32) | 13,048 | 51 KB | `cold_stage2.onnx`, 5 seeds in one graph, 319 KB |
267
- | **Engine v2.0 warm model** (DeepFM: 16-wide embeddings β†’ MLP 64 β†’ 32) | 66,457 | 260 KB | `warm_deepfm.onnx`, 5 seeds in one graph, 1.4 MB |
268
- | Engine v1.1 cold / warm (with community + flair) | 15,170 / 73,979 | 59 / 289 KB | `legacy_v1_1/` |
269
- | Phase-1 LR / Wide&Deep / DeepFM / DCN V2 / SASRec | 37,622 / 74,167 / 111,479 / 189,189 / 30,848 | 0.1–0.7 MB | `phase1_models/` |
270
- | v1 two-stage engine, Stage-2 LR / Wide&Deep / DeepFM | 7,617 / 15,170 / 52,802 | 0.03–0.2 MB | `v1_two_stage_engine/` |
271
-
272
- The `.pth` checkpoints are larger than these numbers because they also store the RecBole configuration and optimizer state.
273
-
274
- **The one large model** in the pipeline is the pre-trained text encoder **[`Qwen/Qwen3-Embedding-0.6B`](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B)** (about 0.6 B parameters, 1.2 GB). It turns post text into the 1024-d content vectors. It is used as is, without fine-tuning, so it is not copied here; download it from the Qwen repository. Image captions in the training data came from Florence-2, also used as is.
275
-
276
- ---
277
-
278
- ## Files in this repo
279
-
280
- ```
281
- README.md this model card
282
- REPORT.md full project report: data, every experiment, all results, code walkthrough
283
- hybrid_engine/
284
- engine.py HybridRecommender: score / rank / recommend, blend, decay, push, topic-first
285
- features.py rebuilds the exact training features from raw post + user data
286
- backend_adapter.py connects a FastAPI + Redis + Postgres feed backend (drop-in build_pool)
287
- example.py runnable demo
288
- tests/ smoke tests, no-community guard, backend adapter tests
289
- README.md, INTEGRATION.md usage and step-by-step backend integration
290
- requirements.txt numpy, onnxruntime
291
- artifacts/
292
- cold_stage2.onnx cold model (Wide&Deep, 5 seeds averaged in one graph)
293
- warm_deepfm.onnx warm model (DeepFM, 5 seeds averaged in one graph)
294
- meta.json vocabularies, feature order, standardisation, PCA, taxonomy, calibration, defaults
295
- build_report.json ONNX == PyTorch checks, test AUC
296
- verification_report.json feature / score parity, replay
297
- backend_modes_report.json ranking quality under backend data conditions
298
- validation_two_sessions.json the two later real sessions
299
- pytorch_checkpoints/engine_v2.0/ the original RecBole checkpoints of the shipped engine (5 seeds per model) for further training
300
- legacy_v1_1/ the previous engine (with community + flair): ONNX, meta.json, reports, pytorch_checkpoints/ (5 seeds per model)
301
- phase1_models/ the five phase-1 models (LR, DeepFM, Wide&Deep, DCN V2, SASRec): ONNX, PyTorch checkpoints,
302
- metadata (vocabularies, standardisation, PCA, export report), Model_Explainer.docx, example_inference.py
303
- v1_two_stage_engine/ the first two-stage engine's Stage-2 models (LR, Wide&Deep, DeepFM): ONNX, checkpoints,
304
- metadata (Stage-1 tag-matcher weights, feature lists, back-test, simulation and tag-quality results)
305
- ```
306
-
307
- **Loading a PyTorch checkpoint** (RecBole 1.2.0): `torch.load(path, weights_only=False)` returns `config` and `state_dict`. Rebuilding the model needs the RecBole datasets from the training kit (GitHub repository, branch `recommender`, `models/training/`).
308
-
309
- ---
310
-
311
- ## Training data and procedure
312
-
313
- - **Data:** Trumio Trace browser telemetry of real Reddit scrolling (impressions, visible/focal attention, hovers, clicks, votes, saves, joins, replies, opened posts).
314
- - 4 consenting participants, 5 sessions, 1,794 exposures, 1,677 posts.
315
- - Usernames hashed, post bodies truncated.
316
- - The training data itself is not published here.
317
- - **Label:** engaged = click / vote / save / join / reply / opened detail, or focal dwell β‰₯ 3 s.
318
- - **Split:** chronological per user, 70% / 15% / 15% (train 1,255 rows, 308 engaged; validation 269, 65; test 270, 44).
319
- - **History features:** causal only. A past interaction counts only if its viewing ended before the request, and only if it happened strictly before it (the serving rule; a training/serving mismatch found here was fixed).
320
- - **Content:** Qwen3-Embedding-0.6B of title + body (+ Florence-2 image caption and OCR), reduced to 16 PCA components.
321
- - **Training:** RecBole 1.2.0, hyperparameters tuned on validation, 5 seeds per model, averaged inside one ONNX graph. The cold model gets Platt calibration fitted on validation.
322
-
323
- ---
324
-
325
- ## Limitations and responsible use
326
-
327
- - **Small data.** 4 users and 44 positive test examples: most differences below about 0.1 AUC are within noise. Re-tune `w_max`, `n0`, decay and push on your own logs, and retrain once you have data from your platform.
328
- - **Trained on Reddit.** Popularity scales, post types and engagement patterns of other platforms differ.
329
- - **Topic-first can hide drifting interests.** If a user's engagement moves to topics outside their picked tags, the 7-of-10 rule pushes those posts down. Keep an exploration share (15–20%) in your serving layer, and monitor off-topic engagement.
330
- - **About 24% of posts got no tag** in the trace. Untagged posts get no closeness bonus but are still ranked by the models.
331
- - **It ranks; it does not retrieve or moderate.** Candidate retrieval, safety filtering and policy enforcement must happen before the engine.
332
-
333
- ---
334
-
335
- ## Citation
336
- ```bibtex
337
- @misc{unihive_recsys_2026,
338
- title = {UniHive RecSys: a hybrid cold-to-warm feed-ranking engine},
339
- author = {Singh, Tirthanker},
340
- year = {2026},
341
- url = {https://huggingface.co/Tirthanker/UniHive_RecSys}
342
- }
343
- ```
344
- Underlying methods: Cheng et al. 2016 (Wide & Deep); Guo et al. 2017 (DeepFM); Wang et al. 2021 (DCN V2); Kang & McAuley 2018 (SASRec); Yi et al. 2019 (two-tower sampling-bias correction); Platt 1999 (calibration); Carbonell & Goldstein 1998 (MMR); Zhao et al. 2021 (RecBole).
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: onnx
4
+ language:
5
+ - en
6
+ pipeline_tag: tabular-classification
7
+ tags:
8
+ - recommender-system
9
+ - feed-ranking
10
+ - ctr-prediction
11
+ - cold-start
12
+ - deepfm
13
+ - wide-and-deep
14
+ - onnx
15
+ ---
16
+
17
+ # UniHive RecSys: hybrid feed-ranking engine (v2.0)
18
+
19
+ A small, fast **post recommendation engine** for a student social app. Give it a user (interests + past interactions) and a list of candidate posts; it returns the posts in feed order, with a score and an explanation for each.
20
+
21
+ > ### βœ… This repository is engine v2.0: our final, intended design. Use `hybrid_engine/`.
22
+ > - **Run it:** `hybrid_engine/` (code) + `hybrid_engine/artifacts/` (the weights it loads). Nothing else is needed.
23
+ > - **Train it further:** `pytorch_checkpoints/engine_v2.0/` (the same v2.0 models as PyTorch files).
24
+ > - **Ignore `archive/`** unless you are studying the project's history. Those are superseded experiments, built on different inputs, and they will not work with `hybrid_engine/`.
25
+
26
+ ---
27
+
28
+ ## Contents
29
+ 1. [Which files you need](#1-which-files-you-need)
30
+ 2. [Install and run in 2 minutes](#2-install-and-run-in-2-minutes)
31
+ 3. [Use it in your code](#3-use-it-in-your-code)
32
+ 4. [Content vectors (recommended)](#4-content-vectors-recommended)
33
+ 5. [Inputs and outputs](#5-inputs-and-outputs)
34
+ 6. [Settings](#6-settings)
35
+ 7. [Plug it into a backend](#7-plug-it-into-a-backend)
36
+ 8. [Continue training](#8-continue-training)
37
+ 9. [How it works](#9-how-it-works)
38
+ 10. [Scores](#10-scores)
39
+ 11. [Model size](#11-model-size)
40
+ 12. [Where the engine's parts come from](#12-where-the-engines-parts-come-from)
41
+ 13. [Training data and limitations](#13-training-data-and-limitations)
42
+ 14. [Archive (older versions)](#14-archive-older-versions)
43
+
44
+ ---
45
+
46
+ ## 1. Which files you need
47
+
48
+ | You want to... | Files |
49
+ |---|---|
50
+ | **Rank posts** (most users) | `hybrid_engine/` (code + `artifacts/cold_stage2.onnx`, `artifacts/warm_deepfm.onnx`, `artifacts/meta.json`) |
51
+ | Turn post text into content vectors | the public encoder [`Qwen/Qwen3-Embedding-0.6B`](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B) (section 4) |
52
+ | Continue training the models | `pytorch_checkpoints/engine_v2.0/` (section 8) |
53
+ | Read the full project report | `REPORT.md` |
54
+
55
+ The engine reads exactly the files in `hybrid_engine/artifacts/`. Don't replace them with files from `archive/`: the feature lists, vocabularies and standardisation differ.
56
+
57
+ ---
58
+
59
+ ## 2. Install and run in 2 minutes
60
+
61
+ ```bash
62
+ pip install huggingface_hub numpy onnxruntime
63
+ hf download Tirthanker/UniHive_RecSys --include "hybrid_engine/*" --local-dir UniHive_RecSys
64
+ cd UniHive_RecSys
65
+ python -m hybrid_engine.example # demo: a brand-new user vs the same user after 30 engaged posts
66
+ python -m hybrid_engine.tests.test_engine # smoke tests; should end with "community / flair guard passed"
67
+ ```
68
+ Python 3.10+. Runs on CPU; no GPU needed.
69
+
70
+ ---
71
+
72
+ ## 3. Use it in your code
73
+
74
+ ```python
75
+ import time
76
+ from hybrid_engine import HybridRecommender
77
+
78
+ engine = HybridRecommender() # load once at startup; reuse for every request
79
+
80
+ user = {
81
+ "interests": ["machine-learning", "computer-vision"], # tag slugs the user picked (list: meta.json -> taxonomy.child)
82
+ "history": [ # what the user saw before; may be empty for a new user
83
+ {"post_id": "p1", "ts_ms": 1791141039157, "visible_ms": 5200, "focal_ms": 4800, "hover_ms": 300,
84
+ "clicked": True, "engaged": True, "community": "c-ml", "author": "a-17", "embedding": None},
85
+ ],
86
+ }
87
+ candidates = [
88
+ {"post_id": "p9", "post_type": "text", "score": 42, "comments": 7, "age_hours": 3.5,
89
+ "title": "Fine-tuning ViTs on a laptop GPU", "body": "Notes from a weekend project ...",
90
+ "tags": {"computer-vision": 0.9, "deep-learning": 0.7}, "embedding": None,
91
+ "author": "a-3", "impressions": 0, "community": "c-cv"},
92
+ {"post_id": "p10", "post_type": "image", "score": 300, "comments": 40, "age_hours": 50,
93
+ "title": "Campus fest photos", "body": "", "tags": {"photography": 0.8}, "embedding": None,
94
+ "author": "a-8", "impressions": 120, "community": "c-fest"},
95
+ ]
96
+
97
+ feed = engine.recommend(user, candidates, now_ms=time.time() * 1000, k=10)
98
+ for d in feed:
99
+ print(d["position"], d["post_id"], round(d["score"], 3), "topic" if d["topic_match"] else "", "warm weight", d["warm_weight"])
100
+ ```
101
+ - `recommend(user, candidates, now_ms, k)`: the top k in final feed order.
102
+ - `rank(...)`: every candidate in feed order.
103
+ - `score(...)`: raw scores without the topic-first reordering.
104
+
105
+ `now_ms` is the request time in milliseconds. History items at or after it are ignored automatically.
106
+
107
+ ---
108
+
109
+ ## 4. Content vectors (recommended)
110
+
111
+ The models read a 1024-d content vector per post. Build it **exactly** like this (the same recipe the models were trained on):
112
+
113
+ ```python
114
+ from sentence_transformers import SentenceTransformer # pip install sentence-transformers
115
+ from hybrid_engine.backend_adapter import qwen3_text
116
+
117
+ encoder = SentenceTransformer("Qwen/Qwen3-Embedding-0.6B"); encoder.max_seq_length = 512 # load once
118
+ text = qwen3_text(title, body, image_caption) # "title. body" (+ " Image: caption"); no community, no flair
119
+ post["embedding"] = encoder.encode(text, normalize_embeddings=True)
120
+ ```
121
+ - Compute it once per post when the post is created, and store it.
122
+ - Without vectors (`"embedding": None`) the engine still ranks, but loses the content signal: about 0.02 lower NDCG@10 on average (section 10).
123
+ - A vector of the wrong size (e.g. a 512-d CLIP vector) is ignored on purpose: it lives in a different space.
124
+
125
+ ---
126
+
127
+ ## 5. Inputs and outputs
128
+
129
+ **Candidate post**
130
+
131
+ | Field | Type | Meaning |
132
+ |---|---|---|
133
+ | `post_id` | str | your id |
134
+ | `post_type` | str | `text`, `image`, `video`, `link`, `gallery`, ... |
135
+ | `score`, `comments` | int | likes / upvotes and comment count |
136
+ | `age_hours` | float | hours since the post was created |
137
+ | `title`, `body`, `image_caption` | str | used for length features (and for the content vector, section 4) |
138
+ | `tags` | dict or list | `{slug: confidence}` from the 114-tag taxonomy, or a list of slugs (confidence 1.0) |
139
+ | `embedding` | 1024 floats or None | section 4 |
140
+ | `author`, `community` | str | used only for the user's *own* history with that author / community, never as model inputs |
141
+ | `impressions` | int | how often your platform has shown the post (drives the new-post push) |
142
+ | optional | | `domain`, `language`, `is_ad`, `is_spoiler`, `upvote_ratio`, `awards`, `is_member` |
143
+
144
+ **History item:** `post_id`, `ts_ms` (when shown), `visible_ms`, `focal_ms` (attention), `hover_ms`, `clicked`, `engaged`, `community`, `author`, `embedding`. Set `engaged` to True for dwell β‰₯ 3 s or a click / like / save / join / reply. If you log only one dwell value, use it for both `visible_ms` and `focal_ms`, and set `hover_ms` to 0.
145
+
146
+ **Output per candidate**
147
+ - `score`, `position`, `topic_match`;
148
+ - `v2b`, `cold_probability`, `tag_closeness`;
149
+ - `warm_probability`, `warm_weight`, `engaged_history`;
150
+ - `freshness_multiplier`, `push`, `has_content_embedding`.
151
+
152
+ Log them to explain any feed.
153
+
154
+ ---
155
+
156
+ ## 6. Settings
157
+
158
+ `HybridRecommender(**overrides)`; the defaults are stored in `hybrid_engine/artifacts/meta.json`.
159
+
160
+ | Setting | Default | Effect |
161
+ |---|---|---|
162
+ | `n0_engaged_posts` | 10 | engaged posts at which the warm model reaches half of its maximum share |
163
+ | `w_max` | 0.7 | maximum warm-model share (the rest stays with the new-user model) |
164
+ | `tag_bonus` | 0.1 | weight of interest-tag closeness in the new-user score |
165
+ | `decay_half_life_hours`, `decay_floor` | 720, 0.85 | freshness: a week-old post keeps 98%, a month-old 93%, never below 85% |
166
+ | `push_beta`, `push_max_age_hours` | 0.1, 24 | lift for posts under 24 h old, fading with impressions |
167
+ | `topic_first_slots`, `topic_block` | 7, 10 | 7 of every 10 positions go to posts matching the user's interests (0 = off) |
168
+
169
+ Example: `HybridRecommender(w_max=0.5, topic_first_slots=5)`.
170
+
171
+ ---
172
+
173
+ ## 7. Plug it into a backend
174
+
175
+ `hybrid_engine/backend_adapter.py` maps the data of a FastAPI + Redis + Postgres feed service onto the engine:
176
+ - **Posts:** `post_from_meta`, `post_from_row` (backend posts β†’ candidates).
177
+ - **History:** `history_from_events` (raw VIEW / LONG_VIEW / CLICK / LIKE / SAVE / SKIP / HIDE events β†’ history, merging visits and correcting timestamps).
178
+ - **Interests:** `expand_interests` (broad interests β†’ tags).
179
+ - **Ranking:** `build_pool_hybrid`, a drop-in replacement for a `build_pool(r, uid, state, level)` scoring step.
180
+ - **Vectors in Redis:** `encode_q3`, `fetch_q3`.
181
+
182
+ Step-by-step guide: [`hybrid_engine/INTEGRATION.md`](hybrid_engine/INTEGRATION.md). Speed: about 0.7 ms per candidate on one CPU thread, so a 200-post pool takes about 140 ms.
183
+
184
+ ---
185
+
186
+ ## 8. Continue training
187
+
188
+ `pytorch_checkpoints/engine_v2.0/` holds the RecBole 1.2.0 originals of both models, 5 seeds each (`cold_widedeep_seed0-4.pth`, `warm_deepfm_seed0-4.pth`). The shipped ONNX files average these 5 seeds.
189
+
190
+ ```bash
191
+ pip install torch recbole==1.2.0 # the checkpoints store a RecBole config, so RecBole must be importable to load them
192
+ ```
193
+ ```python
194
+ import torch
195
+ ckpt = torch.load("pytorch_checkpoints/engine_v2.0/warm_deepfm_seed0.pth", map_location="cpu", weights_only=False)
196
+ print(ckpt["config"]["model"], ckpt["config"]["dataset"]) # DeepFM reco_ctr_nc_noid_strict
197
+ state = ckpt["state_dict"] # weights
198
+ ```
199
+ Rebuilding and retraining needs the RecBole datasets and scripts of the training kit: GitHub repository, branch `recommender`, folder `models/training/`. Use `engine_v2/build_engine_v2.py`, then `engine_v2/verify_hybrid_engine.py`. After retraining, re-export **both** ONNX files and `meta.json` together.
200
+
201
+ ---
202
+
203
+ ## 9. How it works
204
+
205
+ ```
206
+ candidate posts ─┬─► tag closeness (8 tag-match features β†’ logistic score)
207
+ β”œβ”€β–Ί COLD model, Wide&Deep: post stats + content + tag match β†’ calibrated P(engage)
208
+ └─► WARM model, DeepFM: post inputs + 27 user-history features β†’ P(engage)
209
+
210
+ v2b = P_cold + 0.1 Β· rank(tag closeness) (the new-user score)
211
+ w = 0 for a user with no engaged posts, else min(0.7, engaged / (engaged + 10))
212
+ blend = (1 βˆ’ w) Β· rank(v2b) + w Β· rank(P_warm)
213
+ score = blend Β· (0.85 + 0.15 Β· 0.5^(age_h / 720)) + 0.1 Β· [age_h ≀ 24] / sqrt(1 + impressions)
214
+ order = in each block of 10 positions: 7 best interest matches, then 3 best of any kind
215
+ ```
216
+ - **New users:** w = 0, so pure v2b.
217
+ - **The warm model takes over gradually:** w = 0.09 after 1 engaged post, 0.33 after 5, 0.50 after 10, and 0.70 at most. The new-user model always keeps at least 30%.
218
+ - **Excluded from the models:** user and post IDs, feed position, scroll speed, community name and flair.
219
+
220
+ ---
221
+
222
+ ## 10. Scores
223
+
224
+ All numbers are for v2.0, on data the models never trained on.
225
+ - **AUC:** chance an engaged post is scored above a non-engaged one (0.5 = random).
226
+ - **NDCG@10:** quality of the top 10 (1.0 = all engaged posts first).
227
+
228
+ **Models** (test split: last 15% of each user's timeline, 270 exposures, 44 engaged)
229
+
230
+ | Model | Validation AUC | Test AUC |
231
+ |---|---|---|
232
+ | Cold model (Wide&Deep, 5-seed ensemble) | 0.777 | **0.602** |
233
+ | Warm model (DeepFM, 5-seed ensemble) | 0.713 | **0.565** |
234
+
235
+ **Ranking quality** (NDCG@10; each feed ranked as one live request)
236
+
237
+ | Feed | v2.0 | v2.0 with typical backend data | same, no content vectors | Random |
238
+ |---|---|---|---|---|
239
+ | Validation period, whole feeds | **0.650** | 0.579 | 0.604 | 0.350 |
240
+ | Validation period, 25-post pages | 0.559 | 0.595 | 0.582 | 0.350 |
241
+ | Test period, whole feeds | **0.340** | 0.329 | 0.268 | 0.201 |
242
+ | Test period, 25-post pages | 0.415 | 0.397 | 0.441 | 0.286 |
243
+ | Later real session 1, one feed | 0.255 | 0.159 | 0.155 | 0.119 |
244
+ | Later real session 1, 25-post pages | 0.468 | 0.486 | 0.362 | 0.282 |
245
+ | Later real session 2, one feed | 0.110 | 0.110 | 0.110 | 0.079 |
246
+ | Later real session 2, 25-post pages | 0.403 | 0.382 | 0.372 | 0.245 |
247
+ | **Mean** | **0.400** | **0.380** | **0.362** | **0.239** |
248
+
249
+ "Typical backend data" means no flair, tags without confidences, a single dwell value and no vote ratio.
250
+
251
+ **The two later real sessions** (recorded two days after all training data)
252
+
253
+ | | Session 1 (89 posts, 11 engaged) | Session 2 (142 posts, 10 engaged) |
254
+ |---|---|---|
255
+ | v2.0: NDCG@10 / engaged in top 10 / AUC | 0.255 / 3 / 0.688 | 0.110 / 1 / 0.498 |
256
+ | Reddit's own feed order | 0.641 / 6 / 0.917 | 0.312 / 3 / 0.773 |
257
+ | Random | 0.124 / 1.1 / 0.50 | 0.070 / 0.6 / 0.50 |
258
+
259
+ **Engineering checks**
260
+ - ONNX equals PyTorch within 2.4e-7.
261
+ - The engine rebuilds the training features from raw data within 0.0021, with 0 category mismatches.
262
+ - Engine scores match the offline predictions within 2.5e-4.
263
+ - 9 automated tests pass.
264
+
265
+ **Compared with the previous version:** v1.1 used the community name and scored higher on its Reddit training data (test AUC 0.692 / 0.671, mean NDCG@10 0.446). Community names don't carry over to a new platform. Under realistic conditions the gap is small: 0.380 for v2.0 vs 0.410 for v1.1, and 0.362 vs 0.369 without content vectors. Details are in `REPORT.md`, section 13.
266
+
267
+ ---
268
+
269
+ ## 11. Model size
270
+
271
+ | Model | Parameters | Shipped file |
272
+ |---|---|---|
273
+ | Cold model (Wide&Deep: 4-wide embeddings β†’ MLP 64 β†’ 32) | 13,048 per seed | `cold_stage2.onnx`, 5 seeds, 319 KB |
274
+ | Warm model (DeepFM: 16-wide embeddings β†’ MLP 64 β†’ 32) | 66,457 per seed | `warm_deepfm.onnx`, 5 seeds, 1.4 MB |
275
+
276
+ The models are small on purpose: they learn from about 1,800 interactions, and a larger network would only memorise them. The `.pth` files are bigger than the parameter count because they also store the training configuration and optimizer state. The only large model in the pipeline is the public **Qwen3-Embedding-0.6B** text encoder (about 1.2 GB). It is used as is and downloaded from its own repository, so it is not copied here.
277
+
278
+ ---
279
+
280
+ ## 12. Where the engine's parts come from
281
+
282
+ This is an independent implementation built from methods used in well-known production recommenders. It is not affiliated with any of these companies.
283
+
284
+ | Part | Method | Known use |
285
+ |---|---|---|
286
+ | Cold model | **Wide & Deep** (Cheng et al., 2016) | Google Play app recommendations (Google) |
287
+ | Warm model | **DeepFM** (Guo et al., 2017) | Huawei App Market CTR prediction (Huawei Noah's Ark Lab) |
288
+ | Cold β†’ warm blend | new-user model handing over to a behavioural model as history grows | the standard cold-start pattern of e-commerce and streaming feeds |
289
+ | Calibration | **Platt scaling** | CTR models in online advertising |
290
+ | Freshness decay | exponential time decay | the family of Hacker News and Reddit "hot" ranking |
291
+ | New-post push | a boost for new items that fades with exposure | initial-audience boosts for new content in short-video feeds such as TikTok |
292
+ | Topic-first slots + exploration share | slot quotas, with 15–20% exploration kept in the serving layer | quota-based feed blending in large feeds (YouTube, Instagram) |
293
+ | Content vectors | **Qwen3-Embedding-0.6B** | Alibaba Qwen's open embedding model |
294
+ | Interest tags | LLM tagging into a closed taxonomy | cold-starting item catalogs with LLM labels |
295
+ | Evaluated during research, not in v2.0 | two-tower retrieval (YouTube), X's Thunder + Phoenix pipeline, MMR diversification, DCN V2 (Google), SASRec | see `REPORT.md` |
296
+
297
+ ---
298
+
299
+ ## 13. Training data and limitations
300
+
301
+ **Data**
302
+ - Browser telemetry of real Reddit scrolling: 4 consenting participants, 1,794 post exposures, 1,677 posts. Usernames were hashed. The data is not published.
303
+ - **Label:** engaged = click / vote / save / join / reply / opened post, or attention β‰₯ 3 s.
304
+ - **Split:** chronological per user (70 / 15 / 15).
305
+ - **No look-ahead:** history features only count interactions that ended before the ranked post was shown.
306
+
307
+ **Limitations**
308
+ - **Small data.** Differences below about 0.1 AUC are within noise. Treat the settings as a starting point and re-tune `w_max`, `n0_engaged_posts` and decay/push on your own logs.
309
+ - **Trained on Reddit.** Other platforms differ; retrain on your own data when you have it.
310
+ - **The topic-first rule can hide a user's new interests.** Keep a 15–20% exploration share in your serving layer.
311
+ - **It ranks; it doesn't retrieve or moderate.** Filter candidates for safety before ranking.
312
+
313
+ ---
314
+
315
+ ## 14. Archive (older versions)
316
+
317
+ Not for use. `archive/` holds superseded research models, kept only so the history in `REPORT.md` can be checked. Everything in this README, and everything `hybrid_engine/` loads, is v2.0. See [`archive/README.md`](archive/README.md).
318
+
319
+ ---
320
+
321
+ ```bibtex
322
+ @misc{unihive_recsys_2026,
323
+ title = {UniHive RecSys: a hybrid cold-to-warm feed-ranking engine},
324
+ author = {Singh, Tirthanker},
325
+ year = {2026},
326
+ url = {https://huggingface.co/Tirthanker/UniHive_RecSys}
327
+ }
328
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
archive/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Archive: earlier models (reference only)
2
+
3
+ **Do not use these files with `hybrid_engine/`.** The current model is v2.0 in `hybrid_engine/artifacts/` (see the main README). The models here were trained on different inputs (they use community name and flair, user/post IDs or feed position) and have their own vocabularies and standardisation. They are kept so the results in `REPORT.md` can be checked and compared.
4
+
5
+ | Folder | What it is | Status |
6
+ |---|---|---|
7
+ | `engine_v1_1/` | the previous hybrid engine (same design, but its models used community name + flair). ONNX, `meta.json`, reports, `pytorch_checkpoints/` (5 seeds per model) | replaced by v2.0 |
8
+ | `phase1_models/` | the first five models (LR, DeepFM, Wide&Deep, DCN V2, SASRec), trained with user/post IDs and feed position. ONNX, checkpoints, metadata, `Model_Explainer.docx`, `example_inference.py` | research baseline |
9
+ | `v1_two_stage_engine/` | Stage-2 models of the first two-stage engine (tag match, then model). ONNX, checkpoints, Stage-1 weights and results | research baseline, outperformed |
10
+
11
+ The engine v1.1 files can be loaded on their own with `HybridRecommender(artifacts_dir="archive/engine_v1_1")`, but that version needs content vectors built with its own text recipe ("title. community. flair. body"), not the v2.0 one.
{legacy_v1_1 β†’ archive/engine_v1_1}/backend_modes_report.json RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/build_report.json RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/cold_stage2.onnx RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/meta.json RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed0.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed1.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed2.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed3.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/cold_widedeep_seed4.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed0.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed1.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed2.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed3.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/pytorch_checkpoints/warm_deepfm_seed4.pth RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/sensitivity_report.json RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/validation_v1_1.json RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/verification_report.json RENAMED
File without changes
{legacy_v1_1 β†’ archive/engine_v1_1}/warm_deepfm.onnx RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/README.md RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/docs/Model_Explainer.docx RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/example_inference.py RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/metadata/ctr_vocab.json RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/metadata/export_report.json RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/metadata/feature_stats.json RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/metadata/pca.json RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/metadata/sasrec_vocab.json RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/onnx/DCNV2.onnx RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/onnx/DeepFM.onnx RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/onnx/LR.onnx RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/onnx/SASRec.onnx RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/onnx/WideDeep.onnx RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/DCNV2.pth RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/DeepFM.pth RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/LR.pth RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/SASRec.pth RENAMED
File without changes
{phase1_models β†’ archive/phase1_models}/pytorch_checkpoints/WideDeep.pth RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/engine_features.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/results_real_backtest.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/results_simulation.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/results_tag_quality.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage1.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_DeepFM_meta.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_LR_meta.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_WideDeep_meta.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/metadata/stage2_summary.json RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/onnx/DeepFM.onnx RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/onnx/LR.onnx RENAMED
File without changes
{v1_two_stage_engine β†’ archive/v1_two_stage_engine}/onnx/WideDeep.onnx RENAMED
File without changes