079project 8.0.0 → 9.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/LICENSE +165 -0
- package/README.en.md +81 -1
- package/README.md +85 -1
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/dump.rdb +0 -0
- package/groupWorker.cjs +253 -0
- package/inferenceWorker.cjs +94 -0
- package/main.cjs +1263 -173
- package/mainFailedOfJing1Xi4Hua4Zhi4Duan3Yu3.cjs +6320 -0
- package/optimization.cjs +720 -0
- package/package.json +3 -2
- package/test_automatic/answer.csv +401 -0
- package/test_automatic/generate_daily_qa.py +645 -0
- package/test_automatic/question.csv +401 -0
- package/test_automatic.cjs +441 -0
package/optimization.cjs
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TEST = path.join(__dirname, 'test_automatic.cjs');
|
|
8
|
+
const DEFAULT_MAIN_MODULE = path.join(__dirname, 'main.cjs');
|
|
9
|
+
|
|
10
|
+
const nowStamp = () => {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
13
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ensureDir = (dir) => {
|
|
17
|
+
if (!dir) return;
|
|
18
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const parseArgs = (argv) => {
|
|
22
|
+
const out = { _: [], mainArgs: [] };
|
|
23
|
+
for (const item of argv) {
|
|
24
|
+
if (item === '-h' || item === '--help') {
|
|
25
|
+
out.help = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!item.startsWith('--')) {
|
|
29
|
+
out._.push(item);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const eq = item.indexOf('=');
|
|
33
|
+
const key = eq === -1 ? item.slice(2) : item.slice(2, eq);
|
|
34
|
+
const value = eq === -1 ? true : item.slice(eq + 1);
|
|
35
|
+
|
|
36
|
+
if (key === 'main-arg') {
|
|
37
|
+
// allow repeated --main-arg=--foo=bar
|
|
38
|
+
out.mainArgs.push(String(value));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(out, key)) {
|
|
43
|
+
// allow repeated keys by array
|
|
44
|
+
if (Array.isArray(out[key])) out[key].push(value);
|
|
45
|
+
else out[key] = [out[key], value];
|
|
46
|
+
} else {
|
|
47
|
+
out[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const printHelp = ({ mainDefault, testDefault } = {}) => {
|
|
54
|
+
const mainPath = mainDefault || DEFAULT_MAIN_MODULE;
|
|
55
|
+
const testPath = testDefault || DEFAULT_TEST;
|
|
56
|
+
const lines = [
|
|
57
|
+
'Usage:',
|
|
58
|
+
` node optimization.cjs [options]`,
|
|
59
|
+
'',
|
|
60
|
+
'Quick start (recommended):',
|
|
61
|
+
` node optimization.cjs --iters=12 --robots-limit=10000000 --params=decayK,iteration,memeNgramMin,memeNgramMax,minOverlapThreshold,maxMemeWords`,
|
|
62
|
+
'',
|
|
63
|
+
'How to choose command-line parameters:',
|
|
64
|
+
' 1) --params: choose which numeric model params to optimize.',
|
|
65
|
+
' - Valid keys come ONLY from main.cjs exported MODEL_DEFAULTS (no invented keys).',
|
|
66
|
+
' - Example: --params=decayK,iteration,memeNgramMin,memeNgramMax,minOverlapThreshold,maxMemeWords',
|
|
67
|
+
' 2) --init-json: set your starting point (full/partial) before optimization.',
|
|
68
|
+
' - Example: --init-json={"decayK":1.2,"iteration":6}',
|
|
69
|
+
' 3) --cats: choose categorical params to sweep occasionally (if supported by main exports).',
|
|
70
|
+
' - Example: --cats=activationType,transferType',
|
|
71
|
+
' 4) Keep eval comparable by default: learning is frozen unless you pass --allow-learning.',
|
|
72
|
+
' 5) Use --main-arg to forward CLI args to test_automatic/main if needed (repeatable).',
|
|
73
|
+
' 6) Deterministic control-variable search:',
|
|
74
|
+
' - Numeric params are scanned in the exact order of --params.',
|
|
75
|
+
' - Each numeric param is swept on a grid (float step defaults to 0.1).',
|
|
76
|
+
' - Categorical params (e.g. activationType) are tried in order, and for each choice we run a numeric sweep.',
|
|
77
|
+
'',
|
|
78
|
+
'Options:',
|
|
79
|
+
` -h, --help Show this help and exit`,
|
|
80
|
+
` --test=FILE Path to evaluator (default: ${testPath})`,
|
|
81
|
+
` --main=FILE Path to main module that exports MODEL_DEFAULTS (default: ${mainPath})`,
|
|
82
|
+
` --robots-limit=N Forwarded to evaluator as --robots-limit (default: 10000000)`,
|
|
83
|
+
` --host=HOST Evaluator target host (optional)`,
|
|
84
|
+
` --port=PORT Evaluator target port (optional)`,
|
|
85
|
+
` --pass=SCORE Pass threshold forwarded to evaluator as --pass (optional)`,
|
|
86
|
+
` --timeout=MS Kill eval process after timeout (default: 1200000)`,
|
|
87
|
+
` --allow-learning=true Allow learning during eval (default: frozen/disabled learning)`,
|
|
88
|
+
` --main-arg=--k=v Forward arg (repeatable). Example: --main-arg=--disable-adv=true`,
|
|
89
|
+
` --grid=FLOAT Grid step for float params (default: 0.1)`,
|
|
90
|
+
` --grid-max=N Max grid points per param (default: 200; step auto-expands if range too wide)`,
|
|
91
|
+
'',
|
|
92
|
+
'Optimization settings:',
|
|
93
|
+
` --iters=N Passes over all params (default: 12)`,
|
|
94
|
+
` --seed=N RNG seed (kept for logging/compat) (default: 1337)`,
|
|
95
|
+
` --categorical-every=N Try categorical vars on iter=1 and then every N iters (default: 2; 0 disables)`,
|
|
96
|
+
'',
|
|
97
|
+
'Logging:',
|
|
98
|
+
` --log-dir=DIR Output directory (default: ./optimization_logs)`,
|
|
99
|
+
` --run=ID Run id (default: timestamp)`,
|
|
100
|
+
'',
|
|
101
|
+
'Notes:',
|
|
102
|
+
' - This script evaluates by spawning the evaluator (test_automatic.cjs).',
|
|
103
|
+
' - All model params are applied via evaluator feature (--model.* -> /api/model/params).'
|
|
104
|
+
];
|
|
105
|
+
console.log(lines.join('\n'));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const coerceScalar = (value) => {
|
|
109
|
+
if (value === true) return true;
|
|
110
|
+
if (value === false) return false;
|
|
111
|
+
if (value == null) return value;
|
|
112
|
+
const s = String(value).trim();
|
|
113
|
+
if (!s) return s;
|
|
114
|
+
const low = s.toLowerCase();
|
|
115
|
+
if (low === 'true' || low === 'on' || low === 'yes') return true;
|
|
116
|
+
if (low === 'false' || low === 'off' || low === 'no') return false;
|
|
117
|
+
if (/^-?\d+(?:\.\d+)?$/.test(s)) return Number(s);
|
|
118
|
+
return s;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const clamp = (x, min, max) => {
|
|
122
|
+
if (!Number.isFinite(x)) return min;
|
|
123
|
+
return Math.max(min, Math.min(max, x));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const coerceNumber = (v, fallback) => {
|
|
127
|
+
const n = Number(v);
|
|
128
|
+
return Number.isFinite(n) ? n : fallback;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Build a deterministic grid of candidate values for one numeric param.
|
|
132
|
+
// For very wide ranges, the step will auto-expand to keep total points <= maxPoints.
|
|
133
|
+
const listGridValues = (spec, { step, maxPoints } = {}) => {
|
|
134
|
+
const min = Number(spec?.min);
|
|
135
|
+
const max = Number(spec?.max);
|
|
136
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max < min) return [];
|
|
137
|
+
|
|
138
|
+
const isInt = Boolean(spec?.isInt);
|
|
139
|
+
let s = Number(step);
|
|
140
|
+
if (!Number.isFinite(s) || s <= 0) s = isInt ? 1 : 0.1;
|
|
141
|
+
|
|
142
|
+
const cap = Math.max(20, Math.floor(coerceNumber(maxPoints, 200)));
|
|
143
|
+
|
|
144
|
+
if (isInt) {
|
|
145
|
+
const stepInt = Math.max(1, Math.round(s));
|
|
146
|
+
const out = [];
|
|
147
|
+
for (let v = Math.ceil(min); v <= Math.floor(max); v += stepInt) {
|
|
148
|
+
out.push(v);
|
|
149
|
+
if (out.length >= cap) break;
|
|
150
|
+
}
|
|
151
|
+
const maxInt = Math.floor(max);
|
|
152
|
+
if (out.length && out[out.length - 1] !== maxInt && out.length < cap) out.push(maxInt);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Float grid
|
|
157
|
+
const rough = Math.floor((max - min) / s) + 1;
|
|
158
|
+
if (rough > cap) {
|
|
159
|
+
s = (max - min) / (cap - 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const out = [];
|
|
163
|
+
let v = min;
|
|
164
|
+
for (let i = 0; i < cap; i++) {
|
|
165
|
+
const vv = Math.min(max, Math.max(min, Number(v.toFixed(10))));
|
|
166
|
+
out.push(vv);
|
|
167
|
+
if (vv >= max) break;
|
|
168
|
+
v += s;
|
|
169
|
+
}
|
|
170
|
+
if (out.length && out[out.length - 1] !== max && out.length < cap) out.push(max);
|
|
171
|
+
return out;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const deepClone = (obj) => JSON.parse(JSON.stringify(obj || {}));
|
|
175
|
+
|
|
176
|
+
const stableStringify = (obj) => {
|
|
177
|
+
const seen = new WeakSet();
|
|
178
|
+
const sortKeys = (v) => {
|
|
179
|
+
if (!v || typeof v !== 'object') return v;
|
|
180
|
+
if (seen.has(v)) return null;
|
|
181
|
+
seen.add(v);
|
|
182
|
+
if (Array.isArray(v)) return v.map(sortKeys);
|
|
183
|
+
const keys = Object.keys(v).sort();
|
|
184
|
+
const out = {};
|
|
185
|
+
for (const k of keys) out[k] = sortKeys(v[k]);
|
|
186
|
+
return out;
|
|
187
|
+
};
|
|
188
|
+
return JSON.stringify(sortKeys(obj), null, 2);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const pickRandom = (arr, k, rng) => {
|
|
192
|
+
const a = arr.slice();
|
|
193
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
194
|
+
const j = Math.floor(rng() * (i + 1));
|
|
195
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
196
|
+
}
|
|
197
|
+
return a.slice(0, Math.max(0, Math.min(k, a.length)));
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const makeRng = (seed) => {
|
|
201
|
+
// xorshift32
|
|
202
|
+
let x = (seed >>> 0) || 0x9e3779b9;
|
|
203
|
+
return () => {
|
|
204
|
+
x ^= x << 13;
|
|
205
|
+
x ^= x >>> 17;
|
|
206
|
+
x ^= x << 5;
|
|
207
|
+
return ((x >>> 0) / 0xffffffff);
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const parseSummary = (combinedOutput) => {
|
|
212
|
+
const text = String(combinedOutput || '');
|
|
213
|
+
const mScore = text.match(/\[test_automatic\]\s+avgScore=([0-9.]+)/i);
|
|
214
|
+
const mPass = text.match(/\[test_automatic\]\s+avgScore=[0-9.]+\s+pass=(\d+)\/(\d+)/i);
|
|
215
|
+
const score = mScore ? Number(mScore[1]) : NaN;
|
|
216
|
+
const passed = mPass ? Number(mPass[1]) : NaN;
|
|
217
|
+
const total = mPass ? Number(mPass[2]) : NaN;
|
|
218
|
+
return {
|
|
219
|
+
avgScore: Number.isFinite(score) ? score : null,
|
|
220
|
+
passed: Number.isFinite(passed) ? passed : null,
|
|
221
|
+
total: Number.isFinite(total) ? total : null
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const runEval = (testFile, {
|
|
226
|
+
robotsLimit,
|
|
227
|
+
frozen,
|
|
228
|
+
forwardMainArgs,
|
|
229
|
+
modelParams,
|
|
230
|
+
passThreshold,
|
|
231
|
+
host,
|
|
232
|
+
port,
|
|
233
|
+
timeoutMs
|
|
234
|
+
}) => {
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
const started = Date.now();
|
|
237
|
+
|
|
238
|
+
const args = [];
|
|
239
|
+
args.push(testFile);
|
|
240
|
+
|
|
241
|
+
if (host) args.push(`--host=${host}`);
|
|
242
|
+
if (port) args.push(`--port=${port}`);
|
|
243
|
+
if (passThreshold != null) args.push(`--pass=${passThreshold}`);
|
|
244
|
+
|
|
245
|
+
// Always maximize corpus as requested
|
|
246
|
+
if (robotsLimit != null) args.push(`--robots-limit=${robotsLimit}`);
|
|
247
|
+
|
|
248
|
+
// Freeze learning by default to keep evaluation comparable across steps
|
|
249
|
+
if (frozen) args.push('--disable-learning=true');
|
|
250
|
+
|
|
251
|
+
for (const a of (forwardMainArgs || [])) {
|
|
252
|
+
if (a && String(a).startsWith('--')) args.push(String(a));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Model patch via test_automatic feature
|
|
256
|
+
for (const [k, v] of Object.entries(modelParams || {})) {
|
|
257
|
+
const raw = typeof v === 'string' ? v : String(v);
|
|
258
|
+
args.push(`--model.${k}=${raw}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const child = spawn(process.execPath, args, {
|
|
262
|
+
cwd: __dirname,
|
|
263
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
264
|
+
env: { ...process.env }
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
let out = '';
|
|
268
|
+
let err = '';
|
|
269
|
+
|
|
270
|
+
const killTimer = timeoutMs
|
|
271
|
+
? setTimeout(() => {
|
|
272
|
+
try {
|
|
273
|
+
child.kill('SIGKILL');
|
|
274
|
+
} catch (_e) {
|
|
275
|
+
// ignore
|
|
276
|
+
}
|
|
277
|
+
}, timeoutMs)
|
|
278
|
+
: null;
|
|
279
|
+
|
|
280
|
+
child.stdout.on('data', (d) => {
|
|
281
|
+
out += d.toString('utf8');
|
|
282
|
+
});
|
|
283
|
+
child.stderr.on('data', (d) => {
|
|
284
|
+
err += d.toString('utf8');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
child.on('close', (code) => {
|
|
288
|
+
if (killTimer) clearTimeout(killTimer);
|
|
289
|
+
const elapsedMs = Date.now() - started;
|
|
290
|
+
const summary = parseSummary(out + '\n' + err);
|
|
291
|
+
resolve({
|
|
292
|
+
code,
|
|
293
|
+
elapsedMs,
|
|
294
|
+
stdout: out,
|
|
295
|
+
stderr: err,
|
|
296
|
+
...summary
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const loadMainModelSpec = (mainModulePath) => {
|
|
303
|
+
try {
|
|
304
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
305
|
+
const mod = require(mainModulePath);
|
|
306
|
+
const defaults = mod?.MODEL_DEFAULTS && typeof mod.MODEL_DEFAULTS === 'object' ? mod.MODEL_DEFAULTS : null;
|
|
307
|
+
const activations = Array.isArray(mod?.BUILTIN_ACTIVATION_TYPES) ? mod.BUILTIN_ACTIVATION_TYPES : null;
|
|
308
|
+
const transfers = Array.isArray(mod?.BUILTIN_TRANSFER_TYPES) ? mod.BUILTIN_TRANSFER_TYPES : null;
|
|
309
|
+
return { defaults, activations, transfers };
|
|
310
|
+
} catch (e) {
|
|
311
|
+
return { defaults: null, activations: null, transfers: null, error: e };
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// 仅基于 main.cjs 真实存在的参数键生成可搜索空间(不虚构参数名)。
|
|
316
|
+
// 这里的 min/max/eps/step 是优化器的超参,不属于 main 的“参数选项”。
|
|
317
|
+
const buildSearchSpaceFromDefaults = (modelDefaults, { activationTypes, transferTypes } = {}) => {
|
|
318
|
+
const numeric = {};
|
|
319
|
+
const categorical = {};
|
|
320
|
+
|
|
321
|
+
if (!modelDefaults || typeof modelDefaults !== 'object') {
|
|
322
|
+
return { numeric, categorical };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const keys = Object.keys(modelDefaults);
|
|
326
|
+
for (const k of keys) {
|
|
327
|
+
const v = modelDefaults[k];
|
|
328
|
+
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
329
|
+
const isInt = Number.isInteger(v);
|
|
330
|
+
// Heuristic bounds per common param family
|
|
331
|
+
let min;
|
|
332
|
+
let max;
|
|
333
|
+
let eps;
|
|
334
|
+
let step;
|
|
335
|
+
|
|
336
|
+
if (k === 'decayK') {
|
|
337
|
+
min = 0;
|
|
338
|
+
max = 12;
|
|
339
|
+
eps = 0.2;
|
|
340
|
+
step = 0.6;
|
|
341
|
+
} else if (k === 'decayFactor') {
|
|
342
|
+
min = 0.01;
|
|
343
|
+
max = 0.995;
|
|
344
|
+
eps = 0.03;
|
|
345
|
+
step = 0.08;
|
|
346
|
+
} else if (k === 'maliciousThreshold') {
|
|
347
|
+
min = 0;
|
|
348
|
+
max = 1;
|
|
349
|
+
eps = 0.05;
|
|
350
|
+
step = 0.1;
|
|
351
|
+
} else if (k === 'edgeWeight') {
|
|
352
|
+
min = 0.05;
|
|
353
|
+
max = 8;
|
|
354
|
+
eps = 0.25;
|
|
355
|
+
step = 0.6;
|
|
356
|
+
} else if (k.toLowerCase().includes('ngram')) {
|
|
357
|
+
min = 1;
|
|
358
|
+
max = 10;
|
|
359
|
+
eps = 1;
|
|
360
|
+
step = 1;
|
|
361
|
+
} else if (k.toLowerCase().includes('threshold')) {
|
|
362
|
+
min = 0;
|
|
363
|
+
max = 20;
|
|
364
|
+
eps = 1;
|
|
365
|
+
step = 1;
|
|
366
|
+
} else if (k.toLowerCase().includes('iteration')) {
|
|
367
|
+
min = 1;
|
|
368
|
+
max = 20;
|
|
369
|
+
eps = 1;
|
|
370
|
+
step = 1;
|
|
371
|
+
} else if (k.toLowerCase().includes('len')) {
|
|
372
|
+
min = 4;
|
|
373
|
+
max = 256;
|
|
374
|
+
eps = 4;
|
|
375
|
+
step = 4;
|
|
376
|
+
} else if (k.toLowerCase().includes('max')) {
|
|
377
|
+
min = 8;
|
|
378
|
+
max = 5000;
|
|
379
|
+
eps = isInt ? 8 : 0.5;
|
|
380
|
+
step = isInt ? 16 : 1;
|
|
381
|
+
} else if (k.toLowerCase().includes('decay')) {
|
|
382
|
+
min = 0;
|
|
383
|
+
max = 10;
|
|
384
|
+
eps = 0.25;
|
|
385
|
+
step = 0.5;
|
|
386
|
+
} else {
|
|
387
|
+
// default numeric range around default value
|
|
388
|
+
min = isInt ? 0 : Math.min(0, v - Math.abs(v) * 2);
|
|
389
|
+
max = isInt ? Math.max(4, v * 4 + 10) : Math.max(1, v + Math.abs(v) * 3 + 1);
|
|
390
|
+
eps = isInt ? 1 : 0.1;
|
|
391
|
+
step = isInt ? 1 : 0.25;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
numeric[k] = { min, max, eps, step, isInt };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (Array.isArray(activationTypes) && activationTypes.length) {
|
|
399
|
+
categorical.activationType = activationTypes.slice();
|
|
400
|
+
}
|
|
401
|
+
if (Array.isArray(transferTypes) && transferTypes.length) {
|
|
402
|
+
categorical.transferType = transferTypes.slice();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Only keep categorical keys that actually exist in defaults
|
|
406
|
+
for (const k of Object.keys(categorical)) {
|
|
407
|
+
if (!(k in modelDefaults)) {
|
|
408
|
+
delete categorical[k];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { numeric, categorical };
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const normalizeParams = (params, space) => {
|
|
416
|
+
const out = { ...(params || {}) };
|
|
417
|
+
|
|
418
|
+
const numericSpace = space?.numeric && typeof space.numeric === 'object' ? space.numeric : {};
|
|
419
|
+
for (const [k, spec] of Object.entries(numericSpace)) {
|
|
420
|
+
if (out[k] == null) continue;
|
|
421
|
+
let v = Number(out[k]);
|
|
422
|
+
if (!Number.isFinite(v)) continue;
|
|
423
|
+
v = clamp(v, spec.min, spec.max);
|
|
424
|
+
if (spec.isInt) v = Math.round(v);
|
|
425
|
+
out[k] = v;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// dependency: memeNgramMin <= memeNgramMax
|
|
429
|
+
if (out.memeNgramMin != null && out.memeNgramMax != null) {
|
|
430
|
+
if (Number(out.memeNgramMin) > Number(out.memeNgramMax)) {
|
|
431
|
+
const tmp = out.memeNgramMin;
|
|
432
|
+
out.memeNgramMin = out.memeNgramMax;
|
|
433
|
+
out.memeNgramMax = tmp;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return out;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const validateNoUnknownKeys = ({ obj, allowedKeys, label }) => {
|
|
441
|
+
if (!obj || typeof obj !== 'object') return;
|
|
442
|
+
const unknown = Object.keys(obj).filter((k) => !allowedKeys.has(k));
|
|
443
|
+
if (unknown.length) {
|
|
444
|
+
const msg = `[optimization] ${label} contains unknown keys not in main.cjs modelDefaults: ${unknown.join(', ')}`;
|
|
445
|
+
throw new Error(msg);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const main = async () => {
|
|
450
|
+
const args = parseArgs(process.argv.slice(2));
|
|
451
|
+
|
|
452
|
+
if (args.help || args.h === true) {
|
|
453
|
+
printHelp({ mainDefault: DEFAULT_MAIN_MODULE, testDefault: DEFAULT_TEST });
|
|
454
|
+
process.exit(0);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const testFile = path.resolve(String(args.test || DEFAULT_TEST));
|
|
458
|
+
if (!fs.existsSync(testFile)) {
|
|
459
|
+
console.error('[optimization] test_automatic.cjs not found:', testFile);
|
|
460
|
+
process.exit(2);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const mainModulePath = path.resolve(String(args.main || DEFAULT_MAIN_MODULE));
|
|
464
|
+
const spec = loadMainModelSpec(mainModulePath);
|
|
465
|
+
if (!spec.defaults) {
|
|
466
|
+
console.error('[optimization] failed to load MODEL_DEFAULTS from main.cjs.');
|
|
467
|
+
console.error('[optimization] expected main.cjs to export MODEL_DEFAULTS/BUILTIN_ACTIVATION_TYPES/BUILTIN_TRANSFER_TYPES.');
|
|
468
|
+
if (spec.error) console.error('[optimization] load error:', spec.error.message);
|
|
469
|
+
process.exit(3);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const SPACE = buildSearchSpaceFromDefaults(spec.defaults, {
|
|
473
|
+
activationTypes: spec.activations,
|
|
474
|
+
transferTypes: spec.transfers
|
|
475
|
+
});
|
|
476
|
+
const allowedKeys = new Set(Object.keys(spec.defaults));
|
|
477
|
+
|
|
478
|
+
const robotsLimit = Number(args['robots-limit'] ?? 10000000);
|
|
479
|
+
const frozen = String(args['allow-learning'] || '').trim() ? false : true;
|
|
480
|
+
|
|
481
|
+
const host = args.host ? String(args.host) : undefined;
|
|
482
|
+
const port = args.port != null ? Number(args.port) : undefined;
|
|
483
|
+
|
|
484
|
+
const iterations = Math.max(1, Number(args.iters ?? 12));
|
|
485
|
+
const categoricalEvery = Math.max(0, Number(args['categorical-every'] ?? 2));
|
|
486
|
+
const pass = args.pass != null ? Number(args.pass) : undefined;
|
|
487
|
+
const timeoutMs = args.timeout != null ? Number(args.timeout) : 20 * 60 * 1000;
|
|
488
|
+
|
|
489
|
+
const seed = Number(args.seed ?? 1337);
|
|
490
|
+
// NOTE: optimizer is deterministic in coordinate/grid mode; seed kept for logging/compat.
|
|
491
|
+
|
|
492
|
+
const gridStep = coerceNumber(args.grid, 0.1);
|
|
493
|
+
const gridMax = Math.max(20, Math.floor(coerceNumber(args['grid-max'], 200)));
|
|
494
|
+
|
|
495
|
+
const logDir = path.resolve(String(args['log-dir'] || path.join(__dirname, 'optimization_logs')));
|
|
496
|
+
ensureDir(logDir);
|
|
497
|
+
|
|
498
|
+
const runId = String(args.run || nowStamp());
|
|
499
|
+
const logFile = path.join(logDir, `opt_${runId}.jsonl`);
|
|
500
|
+
const bestFile = path.join(logDir, `best_${runId}.json`);
|
|
501
|
+
|
|
502
|
+
const forwardMainArgs = Array.isArray(args.mainArgs) ? args.mainArgs : (args.mainArgs ? [args.mainArgs] : []);
|
|
503
|
+
|
|
504
|
+
let init = {};
|
|
505
|
+
if (args['init-json']) {
|
|
506
|
+
try {
|
|
507
|
+
init = JSON.parse(String(args['init-json']));
|
|
508
|
+
} catch (e) {
|
|
509
|
+
console.warn('[optimization] init-json parse failed:', e.message);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// init-json 允许设置所有真实参数(包括字符串参数),但不允许出现 main 未声明的键
|
|
514
|
+
validateNoUnknownKeys({ obj: init, allowedKeys, label: 'init-json' });
|
|
515
|
+
|
|
516
|
+
// Allow selecting subset of numeric params
|
|
517
|
+
const selectedNumeric = (() => {
|
|
518
|
+
const raw = args.params;
|
|
519
|
+
if (!raw) return Object.keys(SPACE.numeric).slice().sort();
|
|
520
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
521
|
+
const parts = list.flatMap((x) => String(x).split(',').map((s) => s.trim()).filter(Boolean));
|
|
522
|
+
const allowed = new Set(Object.keys(SPACE.numeric));
|
|
523
|
+
const unknown = parts.filter((p) => !allowed.has(p));
|
|
524
|
+
if (unknown.length) {
|
|
525
|
+
throw new Error(`[optimization] --params contains unknown/non-numeric keys: ${unknown.join(', ')}`);
|
|
526
|
+
}
|
|
527
|
+
return parts;
|
|
528
|
+
})();
|
|
529
|
+
|
|
530
|
+
const selectedCategorical = (() => {
|
|
531
|
+
const raw = args.cats;
|
|
532
|
+
if (!raw) return Object.keys(SPACE.categorical).slice().sort();
|
|
533
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
534
|
+
const parts = list.flatMap((x) => String(x).split(',').map((s) => s.trim()).filter(Boolean));
|
|
535
|
+
const allowed = new Set(Object.keys(SPACE.categorical));
|
|
536
|
+
const unknown = parts.filter((p) => !allowed.has(p));
|
|
537
|
+
if (unknown.length) {
|
|
538
|
+
throw new Error(`[optimization] --cats contains unknown/non-categorical keys: ${unknown.join(', ')}`);
|
|
539
|
+
}
|
|
540
|
+
return parts;
|
|
541
|
+
})();
|
|
542
|
+
|
|
543
|
+
const logLine = (obj) => {
|
|
544
|
+
fs.appendFileSync(logFile, JSON.stringify(obj) + '\n');
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// current 包含所有 init-json 提供的真实键;数值键会按 SPACE 进行 clamp/round
|
|
548
|
+
let current = normalizeParams(init, SPACE);
|
|
549
|
+
let best = { params: deepClone(current), score: -Infinity, passed: null, total: null };
|
|
550
|
+
|
|
551
|
+
const evaluate = async (params, meta = {}) => {
|
|
552
|
+
// 不允许出现未知键(避免“虚构参数”)
|
|
553
|
+
validateNoUnknownKeys({ obj: params, allowedKeys, label: 'params' });
|
|
554
|
+
const normalized = normalizeParams(params, SPACE);
|
|
555
|
+
const result = await runEval(testFile, {
|
|
556
|
+
robotsLimit,
|
|
557
|
+
frozen,
|
|
558
|
+
forwardMainArgs,
|
|
559
|
+
modelParams: normalized,
|
|
560
|
+
passThreshold: pass,
|
|
561
|
+
host,
|
|
562
|
+
port,
|
|
563
|
+
timeoutMs
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const entry = {
|
|
567
|
+
t: new Date().toISOString(),
|
|
568
|
+
runId,
|
|
569
|
+
seed,
|
|
570
|
+
meta,
|
|
571
|
+
robotsLimit,
|
|
572
|
+
frozen,
|
|
573
|
+
host: host || null,
|
|
574
|
+
port: port || null,
|
|
575
|
+
passThreshold: pass ?? null,
|
|
576
|
+
elapsedMs: result.elapsedMs,
|
|
577
|
+
exitCode: result.code,
|
|
578
|
+
avgScore: result.avgScore,
|
|
579
|
+
passed: result.passed,
|
|
580
|
+
total: result.total,
|
|
581
|
+
params: normalized
|
|
582
|
+
};
|
|
583
|
+
logLine(entry);
|
|
584
|
+
|
|
585
|
+
if (result.avgScore != null && result.avgScore > best.score) {
|
|
586
|
+
best = { params: deepClone(normalized), score: result.avgScore, passed: result.passed, total: result.total };
|
|
587
|
+
fs.writeFileSync(bestFile, stableStringify({
|
|
588
|
+
runId,
|
|
589
|
+
t: entry.t,
|
|
590
|
+
bestScore: best.score,
|
|
591
|
+
passed: best.passed,
|
|
592
|
+
total: best.total,
|
|
593
|
+
robotsLimit,
|
|
594
|
+
frozen,
|
|
595
|
+
host: host || null,
|
|
596
|
+
port: port || null,
|
|
597
|
+
passThreshold: pass ?? null,
|
|
598
|
+
params: best.params
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return { normalized, result };
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
console.log('[optimization] log:', logFile);
|
|
606
|
+
console.log('[optimization] best:', bestFile);
|
|
607
|
+
console.log('[optimization] main spec:', mainModulePath);
|
|
608
|
+
console.log('[optimization] robots-limit:', robotsLimit);
|
|
609
|
+
console.log('[optimization] frozen (disable-learning):', frozen);
|
|
610
|
+
console.log('[optimization] selected numeric params:', selectedNumeric.join(', '));
|
|
611
|
+
console.log('[optimization] selected categorical params:', selectedCategorical.join(', '));
|
|
612
|
+
console.log('[optimization] grid step (float):', gridStep);
|
|
613
|
+
console.log('[optimization] grid max points:', gridMax);
|
|
614
|
+
|
|
615
|
+
// baseline
|
|
616
|
+
const baseEval = await evaluate(current, { kind: 'baseline' });
|
|
617
|
+
let baseScore = baseEval.result.avgScore ?? -Infinity;
|
|
618
|
+
console.log(`[optimization] baseline avgScore=${baseEval.result.avgScore} pass=${baseEval.result.passed}/${baseEval.result.total}`);
|
|
619
|
+
|
|
620
|
+
const coordinateSweepNumericOnce = async ({ iter, startParams, startScore }) => {
|
|
621
|
+
let local = deepClone(startParams);
|
|
622
|
+
let localScore = startScore;
|
|
623
|
+
|
|
624
|
+
for (const key of selectedNumeric) {
|
|
625
|
+
const spec = SPACE.numeric[key];
|
|
626
|
+
if (!spec) continue;
|
|
627
|
+
|
|
628
|
+
const step = spec.isInt ? 1 : gridStep;
|
|
629
|
+
const values = listGridValues(spec, { step, maxPoints: gridMax });
|
|
630
|
+
if (!values.length) continue;
|
|
631
|
+
|
|
632
|
+
let bestVal = local[key];
|
|
633
|
+
let bestScoreLocal = localScore;
|
|
634
|
+
|
|
635
|
+
console.log(`[optimization] sweep ${key}: candidates=${values.length} (step=${spec.isInt ? 1 : step})`);
|
|
636
|
+
for (const v of values) {
|
|
637
|
+
const trial = deepClone(local);
|
|
638
|
+
trial[key] = v;
|
|
639
|
+
const ev = await evaluate(trial, { kind: 'coord', iter, key, v });
|
|
640
|
+
const s = ev.result.avgScore ?? -Infinity;
|
|
641
|
+
if (s >= bestScoreLocal) {
|
|
642
|
+
bestScoreLocal = s;
|
|
643
|
+
bestVal = v;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
local[key] = bestVal;
|
|
648
|
+
local = normalizeParams(local, SPACE);
|
|
649
|
+
localScore = bestScoreLocal;
|
|
650
|
+
console.log(`[optimization] picked ${key}=${bestVal} score=${localScore}`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return { params: local, score: localScore };
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const sweepCategoricals = async ({ iter, startParams, startScore }) => {
|
|
657
|
+
let local = deepClone(startParams);
|
|
658
|
+
let localScore = startScore;
|
|
659
|
+
|
|
660
|
+
for (const catKey of selectedCategorical) {
|
|
661
|
+
const candidates = SPACE?.categorical?.[catKey] || [];
|
|
662
|
+
if (!Array.isArray(candidates) || candidates.length === 0) continue;
|
|
663
|
+
|
|
664
|
+
console.log(`[optimization] categorical ${catKey}: trying ${candidates.length} candidates in order`);
|
|
665
|
+
|
|
666
|
+
let bestParams = deepClone(local);
|
|
667
|
+
let bestScoreLocal = localScore;
|
|
668
|
+
|
|
669
|
+
for (const cand of candidates) {
|
|
670
|
+
const trialBase = deepClone(local);
|
|
671
|
+
trialBase[catKey] = cand;
|
|
672
|
+
|
|
673
|
+
const baseEv2 = await evaluate(trialBase, { kind: 'cat_base', iter, catKey, cand });
|
|
674
|
+
const baseS2 = baseEv2.result.avgScore ?? -Infinity;
|
|
675
|
+
const refined = await coordinateSweepNumericOnce({ iter, startParams: trialBase, startScore: baseS2 });
|
|
676
|
+
|
|
677
|
+
if (refined.score >= bestScoreLocal) {
|
|
678
|
+
bestScoreLocal = refined.score;
|
|
679
|
+
bestParams = refined.params;
|
|
680
|
+
console.log(`[optimization] best so far for ${catKey}: ${cand} score=${bestScoreLocal}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
local = deepClone(bestParams);
|
|
685
|
+
localScore = bestScoreLocal;
|
|
686
|
+
console.log(`[optimization] categorical picked ${catKey}=${local[catKey]} score=${localScore}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return { params: local, score: localScore };
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
for (let iter = 1; iter <= iterations; iter++) {
|
|
693
|
+
console.log(`\n[optimization] iter ${iter}/${iterations} (deterministic coordinate/grid)`);
|
|
694
|
+
|
|
695
|
+
// Categorical sweep: do it on iter=1, and then every N iterations if enabled.
|
|
696
|
+
if (selectedCategorical.length && (iter === 1 || (categoricalEvery > 0 && (iter % categoricalEvery === 0)))) {
|
|
697
|
+
const outCats = await sweepCategoricals({ iter, startParams: current, startScore: baseScore });
|
|
698
|
+
current = deepClone(outCats.params);
|
|
699
|
+
baseScore = outCats.score;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Numeric sweep: one full pass in fixed order.
|
|
703
|
+
const outNum = await coordinateSweepNumericOnce({ iter, startParams: current, startScore: baseScore });
|
|
704
|
+
current = deepClone(outNum.params);
|
|
705
|
+
baseScore = outNum.score;
|
|
706
|
+
|
|
707
|
+
console.log(`[optimization] end iter ${iter}: score=${baseScore}`);
|
|
708
|
+
console.log(`[optimization] bestSoFar score=${best.score} pass=${best.passed}/${best.total}`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
console.log('\n[optimization] finished');
|
|
712
|
+
console.log('[optimization] best score:', best.score);
|
|
713
|
+
console.log('[optimization] best params written:', bestFile);
|
|
714
|
+
console.log('[optimization] full log:', logFile);
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
main().catch((err) => {
|
|
718
|
+
console.error('[optimization] fatal:', err);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
});
|