@wlearn/ensemble 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/weights.js ADDED
@@ -0,0 +1,143 @@
1
+ import { ValidationError } from '@wlearn/core'
2
+
3
+ /**
4
+ * Project vector onto the probability simplex {w: w >= 0, sum(w) = 1}.
5
+ * O(n log n) algorithm from Duchi et al. 2008.
6
+ *
7
+ * @param {Float64Array} v - input vector
8
+ * @returns {Float64Array} - projected vector
9
+ */
10
+ export function projectSimplex(v) {
11
+ const n = v.length
12
+ if (n === 0) return new Float64Array(0)
13
+ if (n === 1) return new Float64Array([1.0])
14
+
15
+ // Sort descending
16
+ const u = new Float64Array(v)
17
+ u.sort()
18
+ u.reverse()
19
+
20
+ const cssv = new Float64Array(n)
21
+ cssv[0] = u[0]
22
+ for (let i = 1; i < n; i++) {
23
+ cssv[i] = cssv[i - 1] + u[i]
24
+ }
25
+
26
+ let rho = -1
27
+ for (let i = 0; i < n; i++) {
28
+ if (u[i] * (i + 1) > cssv[i] - 1) {
29
+ rho = i
30
+ }
31
+ }
32
+
33
+ if (rho < 0) {
34
+ // Fallback: uniform
35
+ const out = new Float64Array(n)
36
+ out.fill(1.0 / n)
37
+ return out
38
+ }
39
+
40
+ const theta = (cssv[rho] - 1.0) / (rho + 1.0)
41
+ const out = new Float64Array(n)
42
+ for (let i = 0; i < n; i++) {
43
+ out[i] = Math.max(v[i] - theta, 0.0)
44
+ }
45
+ return out
46
+ }
47
+
48
+ /**
49
+ * Optimize ensemble weights via projected gradient descent on the simplex.
50
+ *
51
+ * Classification: minimizes negative log-loss.
52
+ * Regression: minimizes MSE.
53
+ *
54
+ * @param {Float64Array[]} oofPredictions - per-model OOF predictions
55
+ * @param {TypedArray} yTrue - true labels
56
+ * @param {Float64Array} initWeights - initial weights (e.g. from Caruana)
57
+ * @param {object} opts
58
+ * @returns {Float64Array} - optimized weights (>= 0, sum = 1)
59
+ */
60
+ export function optimizeWeights(oofPredictions, yTrue, initWeights, {
61
+ task = 'classification',
62
+ lr = 0.05,
63
+ nIter = 100,
64
+ } = {}) {
65
+ const nModels = oofPredictions.length
66
+ const n = yTrue.length
67
+
68
+ if (nModels === 0) {
69
+ throw new ValidationError('optimizeWeights: need at least 1 model')
70
+ }
71
+ if (nModels === 1) {
72
+ return new Float64Array([1.0])
73
+ }
74
+
75
+ const w = new Float64Array(initWeights)
76
+ const eps = 1e-15
77
+
78
+ if (task === 'classification') {
79
+ const predLen = oofPredictions[0].length
80
+ const nc = predLen / n
81
+ if (nc !== Math.floor(nc)) {
82
+ throw new ValidationError('optimizeWeights: prediction length must be divisible by n')
83
+ }
84
+
85
+ for (let iter = 0; iter < nIter; iter++) {
86
+ const grad = new Float64Array(nModels)
87
+
88
+ for (let i = 0; i < n; i++) {
89
+ const c = yTrue[i] | 0
90
+ // Ensemble probability for true class
91
+ let pTrue = 0
92
+ for (let m = 0; m < nModels; m++) {
93
+ pTrue += w[m] * oofPredictions[m][i * nc + c]
94
+ }
95
+ pTrue = Math.max(pTrue, eps)
96
+
97
+ for (let m = 0; m < nModels; m++) {
98
+ grad[m] -= oofPredictions[m][i * nc + c] / pTrue
99
+ }
100
+ }
101
+
102
+ // Normalize gradient
103
+ for (let m = 0; m < nModels; m++) {
104
+ grad[m] /= n
105
+ }
106
+
107
+ // Gradient step + project
108
+ for (let m = 0; m < nModels; m++) {
109
+ w[m] -= lr * grad[m]
110
+ }
111
+ const proj = projectSimplex(w)
112
+ for (let m = 0; m < nModels; m++) w[m] = proj[m]
113
+ }
114
+ } else {
115
+ // Regression: minimize MSE
116
+ for (let iter = 0; iter < nIter; iter++) {
117
+ const grad = new Float64Array(nModels)
118
+
119
+ for (let i = 0; i < n; i++) {
120
+ let pred = 0
121
+ for (let m = 0; m < nModels; m++) {
122
+ pred += w[m] * oofPredictions[m][i]
123
+ }
124
+ const residual = yTrue[i] - pred
125
+ for (let m = 0; m < nModels; m++) {
126
+ grad[m] -= 2 * residual * oofPredictions[m][i]
127
+ }
128
+ }
129
+
130
+ for (let m = 0; m < nModels; m++) {
131
+ grad[m] /= n
132
+ }
133
+
134
+ for (let m = 0; m < nModels; m++) {
135
+ w[m] -= lr * grad[m]
136
+ }
137
+ const proj = projectSimplex(w)
138
+ for (let m = 0; m < nModels; m++) w[m] = proj[m]
139
+ }
140
+ }
141
+
142
+ return w
143
+ }