aboutsummaryrefslogtreecommitdiff
path: root/cmds/negcap.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmds/negcap.go')
-rw-r--r--cmds/negcap.go34
1 files changed, 34 insertions, 0 deletions
diff --git a/cmds/negcap.go b/cmds/negcap.go
new file mode 100644
index 0000000..f531b14
--- /dev/null
+++ b/cmds/negcap.go
@@ -0,0 +1,34 @@
+// Per-feed negative ceiling, shared by `export` (enforced while sampling)
+// and `compact-labels` (enforced against existing history).
+//
+// Why: negatives are sampled from the read+unstarred pool, whose size is
+// dominated by high-volume feeds. Without a ceiling, a handful of firehoses
+// own most of the negative class, and the model learns "wire headline" as
+// its main axis instead of "headline samsc liked". The ceiling is on each
+// feed's *total* negatives (existing history + this run's additions), not
+// just the run's sample: a per-run cap would only slow domination, not
+// bound it.
+package cmds
+
+// negFeedCapFloor is the absolute minimum ceiling. It keeps a young,
+// still-small negative pool from capping feeds at a handful of rows:
+// early training needs volume more than it needs balance.
+const negFeedCapFloor = 50
+
+// feedNegCap returns the maximum negatives any single feed may hold in the
+// pool: pct% of the target (negRatio × posTotal), floored at negFeedCapFloor
+// and never above the target itself. pct <= 0 means uncapped (0).
+func feedNegCap(pct, negRatio, posTotal int) int {
+ if pct <= 0 {
+ return 0
+ }
+ target := negRatio * posTotal
+ cap := pct * target / 100
+ if cap < negFeedCapFloor {
+ cap = negFeedCapFloor
+ }
+ if cap > target {
+ cap = target
+ }
+ return cap
+}