132 lines
5.3 KiB
JavaScript
132 lines
5.3 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* W1 补丁 — 2026-04-16 audit-2026-04-16 WARNING
|
|||
|
|
*
|
|||
|
|
* 问题: simulateSignals 中 context/project/workflow 三维梯度恒为 0,
|
|||
|
|
* bm25/semantic 单向累积导致权重持续漂移 (当前 bm25=0.598, 距 DEFAULT 0.40 漂移 +0.198)。
|
|||
|
|
* 虽有 cooldown 防 reset 抖动,但根因未修,长期仍会趋近 MAX_WEIGHT 边界。
|
|||
|
|
*
|
|||
|
|
* 修复方案: 在两处梯度更新后追加 L2 权重衰减项 (正则化拉回 DEFAULT_WEIGHTS)。
|
|||
|
|
* delta_new = LR * gradient + DECAY * (DEFAULT - current)
|
|||
|
|
* 当 current 远离 DEFAULT → 反向拉力加大
|
|||
|
|
* 当 current 等于 DEFAULT → 无影响
|
|||
|
|
* 稳态: 梯度力 = 衰减力 → spread 有界
|
|||
|
|
*
|
|||
|
|
* 常量: WEIGHT_DECAY = 0.02 (与 LEARNING_RATE 同量级)
|
|||
|
|
* 数学估算: 当前 bm25=0.598 → 每批衰减 0.02*(0.598-0.4)=0.004,
|
|||
|
|
* 足以抵消 simulateSignals 给 bm25 的单向梯度 (~0.02 量级)
|
|||
|
|
*
|
|||
|
|
* 两处更新点:
|
|||
|
|
* - L283 learnFusionWeights 主路径
|
|||
|
|
* - L541 applyImplicitWeights 隐式反馈路径
|
|||
|
|
*
|
|||
|
|
* 幂等: sentinel = 'WEIGHT_DECAY'
|
|||
|
|
*/
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const TARGET = path.join(__dirname, '..', 'fusion-weight-learner.js');
|
|||
|
|
const SENTINEL = 'WEIGHT_DECAY';
|
|||
|
|
|
|||
|
|
function main() {
|
|||
|
|
if (!fs.existsSync(TARGET)) {
|
|||
|
|
console.error('[patch-w1] 目标文件不存在');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
const before = fs.readFileSync(TARGET, 'utf8');
|
|||
|
|
|
|||
|
|
if (before.includes(SENTINEL)) {
|
|||
|
|
console.log('[patch-w1] 已打过补丁 (发现 ' + SENTINEL + '),跳过');
|
|||
|
|
process.exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==== 注入 1: 在 LEARNING_RATE 后追加 WEIGHT_DECAY 常量 ====
|
|||
|
|
const constAnchor = 'const LEARNING_RATE = 0.02;';
|
|||
|
|
const constInjection =
|
|||
|
|
'const LEARNING_RATE = 0.02;\n' +
|
|||
|
|
'// W1 修复 (patch-w1-weight-decay, 2026-04-16): L2 正则化强度\n' +
|
|||
|
|
'// 每批次将权重按比例拉回 DEFAULT_WEIGHTS,防止 simulateSignals 三维=0 导致的单向漂移\n' +
|
|||
|
|
'const WEIGHT_DECAY = 0.02;';
|
|||
|
|
if (!before.includes(constAnchor)) {
|
|||
|
|
console.error('[patch-w1] 找不到 LEARNING_RATE 常量锚点');
|
|||
|
|
process.exit(2);
|
|||
|
|
}
|
|||
|
|
let after = before.replace(constAnchor, constInjection);
|
|||
|
|
|
|||
|
|
// ==== 注入 2: learnFusionWeights L283 梯度更新循环 ====
|
|||
|
|
const mainAnchor =
|
|||
|
|
' // 梯度更新\n' +
|
|||
|
|
' const newWeights = { ...weights };\n' +
|
|||
|
|
' for (const key of Object.keys(newWeights)) {\n' +
|
|||
|
|
' newWeights[key] += LEARNING_RATE * totalGradient[key];\n' +
|
|||
|
|
' }';
|
|||
|
|
const mainReplacement =
|
|||
|
|
' // 梯度更新 + W1 权重衰减 (L2 正则化拉回 DEFAULT_WEIGHTS)\n' +
|
|||
|
|
' const newWeights = { ...weights };\n' +
|
|||
|
|
' for (const key of Object.keys(newWeights)) {\n' +
|
|||
|
|
' newWeights[key] += LEARNING_RATE * totalGradient[key];\n' +
|
|||
|
|
' newWeights[key] += WEIGHT_DECAY * (DEFAULT_WEIGHTS[key] - newWeights[key]);\n' +
|
|||
|
|
' }';
|
|||
|
|
if (!after.includes(mainAnchor)) {
|
|||
|
|
console.error('[patch-w1] 找不到 learnFusionWeights 梯度更新锚点');
|
|||
|
|
process.exit(3);
|
|||
|
|
}
|
|||
|
|
after = after.replace(mainAnchor, mainReplacement);
|
|||
|
|
|
|||
|
|
// ==== 注入 3: applyImplicitWeights L541 梯度更新循环 ====
|
|||
|
|
const implicitAnchor =
|
|||
|
|
' // 应用梯度更新(保守学习率,使用 LEARNING_RATE 的一半避免与 learnFusionWeights 叠加)\n' +
|
|||
|
|
' const implicitLR = LEARNING_RATE * 0.5;\n' +
|
|||
|
|
' const newWeights = { ...currentWeights };\n' +
|
|||
|
|
' for (const key of Object.keys(newWeights)) {\n' +
|
|||
|
|
' newWeights[key] += implicitLR * (totalGradient[key] || 0);\n' +
|
|||
|
|
' }';
|
|||
|
|
const implicitReplacement =
|
|||
|
|
' // 应用梯度更新(保守学习率,使用 LEARNING_RATE 的一半避免与 learnFusionWeights 叠加)\n' +
|
|||
|
|
' // W1: 同步使用 WEIGHT_DECAY 的一半,与 implicitLR 成比例\n' +
|
|||
|
|
' const implicitLR = LEARNING_RATE * 0.5;\n' +
|
|||
|
|
' const implicitDecay = WEIGHT_DECAY * 0.5;\n' +
|
|||
|
|
' const newWeights = { ...currentWeights };\n' +
|
|||
|
|
' for (const key of Object.keys(newWeights)) {\n' +
|
|||
|
|
' newWeights[key] += implicitLR * (totalGradient[key] || 0);\n' +
|
|||
|
|
' newWeights[key] += implicitDecay * (DEFAULT_WEIGHTS[key] - newWeights[key]);\n' +
|
|||
|
|
' }';
|
|||
|
|
if (!after.includes(implicitAnchor)) {
|
|||
|
|
console.error('[patch-w1] 找不到 applyImplicitWeights 梯度更新锚点');
|
|||
|
|
process.exit(4);
|
|||
|
|
}
|
|||
|
|
after = after.replace(implicitAnchor, implicitReplacement);
|
|||
|
|
|
|||
|
|
// ==== 校验 ====
|
|||
|
|
if (after === before) {
|
|||
|
|
console.error('[patch-w1] 替换无效果');
|
|||
|
|
process.exit(5);
|
|||
|
|
}
|
|||
|
|
const decayCount = (after.match(/WEIGHT_DECAY/g) || []).length;
|
|||
|
|
if (decayCount < 4) {
|
|||
|
|
console.error('[patch-w1] WEIGHT_DECAY 引用数异常:', decayCount, '(期望 >= 4)');
|
|||
|
|
process.exit(6);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==== 备份并原子写回 ====
|
|||
|
|
const backup = TARGET + '.bak.w1.' + Date.now();
|
|||
|
|
fs.writeFileSync(backup, before);
|
|||
|
|
const _tmp = TARGET + '.tmp.' + process.pid;
|
|||
|
|
fs.writeFileSync(_tmp, after);
|
|||
|
|
fs.renameSync(_tmp, TARGET);
|
|||
|
|
|
|||
|
|
console.log('[patch-w1] ✓ 补丁应用成功');
|
|||
|
|
console.log('[patch-w1] 备份: ' + path.basename(backup));
|
|||
|
|
console.log('[patch-w1] 变更: 新增 WEIGHT_DECAY=0.02 + 两处梯度循环加衰减项');
|
|||
|
|
console.log('[patch-w1] WEIGHT_DECAY 引用数: ' + decayCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
main();
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('[patch-w1] 异常:', e.message);
|
|||
|
|
process.exit(99);
|
|||
|
|
}
|