bookworm-smart-assistant/scripts/patches/patch-w1-weight-decay.js

132 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
}