@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/package.json +26 -0
- package/src/bagging.js +410 -0
- package/src/index.js +6 -0
- package/src/oof.js +96 -0
- package/src/selection.js +127 -0
- package/src/stacking.js +372 -0
- package/src/voting.js +311 -0
- package/src/weights.js +143 -0
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
|
+
}
|