feat(recommender): LightGBM lambdarank ranker with impression features (M6)
The wave logs each served candidate's feature vector to rec_impression_features. Nightly training joins them with impression outcomes, validates on the last week and activates the model only when it beats the v1 order and the active model. The service loads the active ranker and uses its order in place of the v1 score. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fb07f962eb
commit
58bd14ef38
12 files changed
+551
-14
No files matched your search
@@ -17,6 +17,7 @@ import psycopg
|
||||
from pipu_rec.train.als import ALSModel, normalize, train_als
|
||||
from pipu_rec.train.bandit import update_bandits
|
||||
from pipu_rec.train.metrics import evaluate
|
||||
from pipu_rec.train.ranker import train_and_save
|
||||
from pipu_rec.train.signals import DISLIKE, LIKE, Interaction, event_weight, implicit_scores
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -160,9 +161,19 @@ def train_all(dsn: str, now: float | None = None) -> dict[str, Any]:
|
||||
try:
|
||||
# Бандит считается всегда — ему хватает и пары сессий волны, в отличие от ALS.
|
||||
bandit_users = update_bandits(conn, now)
|
||||
# Ранжировщик v2 учится на показах волны и от числа событий ALS не зависит.
|
||||
try:
|
||||
ranker = train_and_save(conn, now)
|
||||
except Exception as e: # обучение ранжировщика не должно мешать ALS
|
||||
log.exception("ranker training failed")
|
||||
ranker = {"ranker": {"error": str(e)}}
|
||||
interactions = load_interactions(conn)
|
||||
if len(interactions) < MIN_INTERACTIONS:
|
||||
return {"skipped": f"only {len(interactions)} interactions", "bandit_users": bandit_users}
|
||||
return {
|
||||
"skipped": f"only {len(interactions)} interactions",
|
||||
"bandit_users": bandit_users,
|
||||
**ranker,
|
||||
}
|
||||
started = time.monotonic()
|
||||
offline = offline_metrics(interactions, now)
|
||||
model = train_als(implicit_scores(interactions, now))
|
||||
@@ -184,10 +195,10 @@ def train_all(dsn: str, now: float | None = None) -> dict[str, Any]:
|
||||
(version, json.dumps(metrics)),
|
||||
)
|
||||
log.warning("als %s not activated: offline metrics regressed %s", version, offline)
|
||||
return {"version": version, "activated": False, **metrics}
|
||||
return {"version": version, "activated": False, **metrics, **ranker}
|
||||
save_als(conn, model, version, metrics)
|
||||
log.info("als %s activated: %s", version, metrics)
|
||||
return {"version": version, "activated": True, **metrics}
|
||||
return {"version": version, "activated": True, **metrics, **ranker}
|
||||
finally:
|
||||
conn.execute("SELECT pg_advisory_unlock(%s)", (ADVISORY_LOCK,))
|
||||
|
||||
@@ -218,7 +229,8 @@ class NightlyTrainer:
|
||||
try:
|
||||
result = train_all(self.dsn)
|
||||
log.info("training finished: %s", result)
|
||||
if result.get("activated") and self.on_done:
|
||||
ranker_on = bool((result.get("ranker") or {}).get("activated"))
|
||||
if (result.get("activated") or ranker_on) and self.on_done:
|
||||
self.on_done()
|
||||
except Exception:
|
||||
log.exception("training failed")
|
||||
|
||||
Reference in new issue
Block a user