144 lines
6.9 KiB
JavaScript
144 lines
6.9 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* Patch C3: adaptive-disambiguator Dirichlet 根治
|
|||
|
|
* - 懒初始化补 _initialAlphas 快照 (修 updateFromFeedback 路径)
|
|||
|
|
* - 竞争技能软衰减 (避免单调累积 → 权重塌陷)
|
|||
|
|
* - EMA 上限 (Σα ≤ 200,饱和后按比例回缩)
|
|||
|
|
* - adaptiveDisambiguate 归一化改 softmax-lite (不再用 maxAdj 线性缩放)
|
|||
|
|
* 幂等: sentinel C3_DIRICHLET_HARDENING_v1
|
|||
|
|
*/
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const TARGET = path.join(__dirname, '..', 'adaptive-disambiguator.js');
|
|||
|
|
const SENTINEL = 'C3_DIRICHLET_HARDENING_v1';
|
|||
|
|
|
|||
|
|
function main() {
|
|||
|
|
const src = fs.readFileSync(TARGET, 'utf8');
|
|||
|
|
if (src.includes(SENTINEL)) { console.error('[patch-c3] already applied'); process.exit(0); }
|
|||
|
|
|
|||
|
|
let patched = src;
|
|||
|
|
let hits = 0;
|
|||
|
|
|
|||
|
|
// (1) 懒初始化补 _initialAlphas 快照 (L258-267)
|
|||
|
|
const old1 =
|
|||
|
|
" if (!state.pairs[key]) {\r\n" +
|
|||
|
|
" state.pairs[key] = {\r\n" +
|
|||
|
|
" alphas: {\r\n" +
|
|||
|
|
" [actualSkill]: CONFIG.weakPrior,\r\n" +
|
|||
|
|
" [competing]: CONFIG.weakPrior,\r\n" +
|
|||
|
|
" },\r\n" +
|
|||
|
|
" totalSamples: 0,\r\n" +
|
|||
|
|
" lastUpdated: null,\r\n" +
|
|||
|
|
" };\r\n" +
|
|||
|
|
" }";
|
|||
|
|
// 兼容 LF 情况
|
|||
|
|
const old1LF = old1.replace(/\r\n/g, '\n');
|
|||
|
|
let use1 = null;
|
|||
|
|
if (patched.includes(old1)) use1 = old1;
|
|||
|
|
else if (patched.includes(old1LF)) use1 = old1LF;
|
|||
|
|
if (!use1) { console.error('[patch-c3] anchor1 miss'); process.exit(3); }
|
|||
|
|
|
|||
|
|
const NL = use1.includes('\r\n') ? '\r\n' : '\n';
|
|||
|
|
const new1 =
|
|||
|
|
" if (!state.pairs[key]) {" + NL +
|
|||
|
|
" // " + SENTINEL + ": 懒初始化必须保存 _initialAlphas 快照,否则 drift 报警失效" + NL +
|
|||
|
|
" const _alphas = {" + NL +
|
|||
|
|
" [actualSkill]: CONFIG.weakPrior," + NL +
|
|||
|
|
" [competing]: CONFIG.weakPrior," + NL +
|
|||
|
|
" };" + NL +
|
|||
|
|
" state.pairs[key] = {" + NL +
|
|||
|
|
" alphas: _alphas," + NL +
|
|||
|
|
" _initialAlphas: { ..._alphas }," + NL +
|
|||
|
|
" totalSamples: 0," + NL +
|
|||
|
|
" lastUpdated: null," + NL +
|
|||
|
|
" };" + NL +
|
|||
|
|
" }";
|
|||
|
|
patched = patched.replace(use1, new1);
|
|||
|
|
hits++;
|
|||
|
|
|
|||
|
|
// (2) updateFromFeedback α 更新: 加竞争软衰减 + EMA 上限
|
|||
|
|
// 锚点: `pair.alphas[actualSkill] = (pair.alphas[actualSkill] || CONFIG.weakPrior) + 1;`
|
|||
|
|
const old2 = " pair.alphas[actualSkill] = (pair.alphas[actualSkill] || CONFIG.weakPrior) + 1;";
|
|||
|
|
if (!patched.includes(old2)) { console.error('[patch-c3] anchor2 miss'); process.exit(4); }
|
|||
|
|
const new2 =
|
|||
|
|
" // " + SENTINEL + ": 正样本 +1; 竞争技能软衰减 (1%) 防止单调累积; EMA 上限 Σα ≤ 200" + NL +
|
|||
|
|
" pair.alphas[actualSkill] = (pair.alphas[actualSkill] || CONFIG.weakPrior) + 1;" + NL +
|
|||
|
|
" if (pair.alphas[competing] !== undefined && pair.alphas[competing] > CONFIG.weakPrior) {" + NL +
|
|||
|
|
" pair.alphas[competing] = Math.max(CONFIG.weakPrior," + NL +
|
|||
|
|
" pair.alphas[competing] - 0.01 * (pair.alphas[competing] - CONFIG.weakPrior));" + NL +
|
|||
|
|
" }" + NL +
|
|||
|
|
" // EMA 上限: 总样本量超过 200 时按比例回缩 (保持相对比例)" + NL +
|
|||
|
|
" const _sumAlpha = Object.values(pair.alphas).reduce(function (s, a) { return s + a; }, 0);" + NL +
|
|||
|
|
" if (_sumAlpha > 200) {" + NL +
|
|||
|
|
" const _scale = 200 / _sumAlpha;" + NL +
|
|||
|
|
" for (const _k of Object.keys(pair.alphas)) {" + NL +
|
|||
|
|
" pair.alphas[_k] = Math.max(CONFIG.weakPrior, pair.alphas[_k] * _scale);" + NL +
|
|||
|
|
" }" + NL +
|
|||
|
|
" }";
|
|||
|
|
patched = patched.replace(old2, new2);
|
|||
|
|
hits++;
|
|||
|
|
|
|||
|
|
// (3) adaptiveDisambiguate: softmax-lite 归一化
|
|||
|
|
const old3 =
|
|||
|
|
" // 归一化 Bayesian 调整量到 [0, 1] 范围";
|
|||
|
|
if (!patched.includes(old3)) { console.error('[patch-c3] anchor3 miss'); process.exit(5); }
|
|||
|
|
// 精确锚点: 替换从"归一化"注释到"let maxAdj = 0" 块后两行,但为安全只改注释标签,配合下方 result.map 内逻辑替换
|
|||
|
|
// 更稳妥: 直接替换 map 里 `const normalizedAdj = maxAdj > 0 ? adj / maxAdj : 0;`
|
|||
|
|
const old3b = " const normalizedAdj = maxAdj > 0 ? adj / maxAdj : 0;";
|
|||
|
|
if (!patched.includes(old3b)) { console.error('[patch-c3] anchor3b miss'); process.exit(6); }
|
|||
|
|
// 引入 softmax 归一化: 在函数内替换 maxAdj 计算块 + normalizedAdj 计算
|
|||
|
|
const old3Block =
|
|||
|
|
" // 归一化 Bayesian 调整量到 [0, 1] 范围" + NL +
|
|||
|
|
" let maxAdj = 0;" + NL +
|
|||
|
|
" for (const adj of adjustments.values()) {" + NL +
|
|||
|
|
" if (Math.abs(adj) > maxAdj) maxAdj = Math.abs(adj);" + NL +
|
|||
|
|
" }";
|
|||
|
|
const old3BlockLF = old3Block.replace(/\r\n/g, '\n');
|
|||
|
|
let use3 = null;
|
|||
|
|
if (patched.includes(old3Block)) use3 = old3Block;
|
|||
|
|
else if (patched.includes(old3BlockLF)) use3 = old3BlockLF;
|
|||
|
|
if (!use3) { console.error('[patch-c3] anchor3 block miss'); process.exit(7); }
|
|||
|
|
const new3Block =
|
|||
|
|
" // " + SENTINEL + ": softmax-lite 归一化,避免 maxAdj 线性缩放导致的 ±1 饱和" + NL +
|
|||
|
|
" // Σ_i softmax(adj_i) = 1; 映射到 [-1, +1] 区间做 boost 基准" + NL +
|
|||
|
|
" const _expAdj = new Map();" + NL +
|
|||
|
|
" let _sumExp = 0;" + NL +
|
|||
|
|
" for (const [_k, _v] of adjustments.entries()) {" + NL +
|
|||
|
|
" const _e = Math.exp(Math.max(-5, Math.min(5, _v)));" + NL +
|
|||
|
|
" _expAdj.set(_k, _e);" + NL +
|
|||
|
|
" _sumExp += _e;" + NL +
|
|||
|
|
" }" + NL +
|
|||
|
|
" const _n = adjustments.size || 1;" + NL +
|
|||
|
|
" const _uniform = 1 / _n;" + NL +
|
|||
|
|
" // 兼容符号: 保留 maxAdj 供 fallback (若 softmax 退化则退回原算法)" + NL +
|
|||
|
|
" let maxAdj = 0;" + NL +
|
|||
|
|
" for (const adj of adjustments.values()) {" + NL +
|
|||
|
|
" if (Math.abs(adj) > maxAdj) maxAdj = Math.abs(adj);" + NL +
|
|||
|
|
" }";
|
|||
|
|
patched = patched.replace(use3, new3Block);
|
|||
|
|
hits++;
|
|||
|
|
|
|||
|
|
// 并替换 normalizedAdj 使用 softmax 结果
|
|||
|
|
const new3b =
|
|||
|
|
" // " + SENTINEL + ": softmax 归一化 (退化时 fallback 到 maxAdj 线性)" + NL +
|
|||
|
|
" const _soft = _sumExp > 0 ? (_expAdj.get(c.name) || 0) / _sumExp : _uniform;" + NL +
|
|||
|
|
" // 映射 [0,1] → [-1,+1]: (soft - uniform) / uniform,再按 maxAdj 兜底" + NL +
|
|||
|
|
" const _softSigned = (_soft - _uniform) / Math.max(_uniform, 1e-6);" + NL +
|
|||
|
|
" const normalizedAdj = Math.max(-1, Math.min(1, _softSigned));";
|
|||
|
|
patched = patched.replace(old3b, new3b);
|
|||
|
|
hits++;
|
|||
|
|
|
|||
|
|
if (patched === src) { console.error('[patch-c3] no change'); process.exit(8); }
|
|||
|
|
if (hits < 4) { console.error('[patch-c3] expected 4 hits, got', hits); process.exit(9); }
|
|||
|
|
|
|||
|
|
const bakDir = path.join(path.dirname(TARGET), 'patches', 'bak');
|
|||
|
|
if (!fs.existsSync(bakDir)) fs.mkdirSync(bakDir, { recursive: true });
|
|||
|
|
fs.writeFileSync(path.join(bakDir, 'adaptive-disambiguator.js.bak.c3.' + Date.now()), src);
|
|||
|
|
const tmp = TARGET + '.tmp.' + process.pid;
|
|||
|
|
fs.writeFileSync(tmp, patched);
|
|||
|
|
fs.renameSync(tmp, TARGET);
|
|||
|
|
console.error('[patch-c3] applied, hits=' + hits);
|
|||
|
|
}
|
|||
|
|
main();
|