feat: 诊断(任务书 18 第一块):诊断数学、四种提交、一次运行跑多个实验、账本与材料、flow 诊断各步

消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。
eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 10:36:20 -04:00
parent f39f8410e7
commit 852600df02
69 changed files with 6607 additions and 214 deletions
+139 -31
View File
@@ -9,13 +9,44 @@ import {
GRID_SUBDIVISIONS,
improvement,
} from "./basics.ts";
import { ablationCellMixture, below, integrateUpperTail, knotsMean, mapKnots, type Knots } from "./distribution.ts";
import {
ablationCellMixture,
below,
density,
integrateUpperTail,
knotsMean,
mapKnots,
type Knots,
} from "./distribution.ts";
// 方法.md 6.7:方向收益、成分与权重、信息价值、价值减成本。选格规则和押的因子在 cells.ts。
//
// 一律先在"撑几成"刻度 s(x) = [f(完整) − x] ÷ G(按方向折算)上算,最后乘剩余空间 h:
// 收益_p = h × s_p,h 对所有因子相同且为正,所以信息价值、期望收益、ĝ₀ 都等于 h × (撑几成刻度上的同一个量)。
// 例外是部分格网格的切段:在分数原值上取折点、切小段,再把中点换成撑几成(见 measuredGrid)。
// 成分、权重更新、精确积分、部分格网格、价值减成本这套引擎消融(6.7)和诊断(6.12)共用,只差"实测 → 收益"的换算:
// - 消融:一律先在"撑几成"刻度 s(x) = [f(完整) − x] ÷ G(按方向折算)上算,最后乘剩余空间 h:
// 收益_p = h × s_p,h 对所有因子相同且为正,所以信息价值、期望收益、ĝ₀ 都等于 h × (撑几成刻度上的同一个量);
// 不设"都不走"的选项。
// - 诊断(diagnosis.ts):收益 g(x) = 改进量 x − b,h 取 1,另有收益为 0 的"都不走",即 floor = 0。
// 例外是部分格网格的切段:在分数原值上取折点、切小段,再把中点换成收益刻度(见 gridOf)。
/** 引擎要的成分模型(消融和诊断各自建出来)。下标"项"是因子(消融)或实验(诊断)。 */
export interface ValueModel {
/** 成分起始权重。 */
weights: number[];
/** rawKnots[项][成分]:分数原值上的折线。 */
rawKnots: Knots[][];
/** scaledKnots[项][成分]:收益刻度上的折线。 */
scaledKnots: Knots[][];
/** expected[项][成分]:这个成分下这一项在收益刻度上的期望。 */
expected: number[][];
/** prior[项]:按起始权重平均的期望。 */
prior: number[];
/** 收益 = space × 收益刻度上的值:消融是剩余空间 h,诊断是 1。 */
space: number;
/** "都不走"的收益:诊断是 0,消融没有这个选项,写 null。 */
floor: number | null;
/** 分数原值 → 收益刻度。 */
scale: (x: number) => number;
}
/** 开放预留成分的名字(成分名单里排在最后)。 */
export const OPEN_RESERVE = "openReserve";
@@ -70,6 +101,20 @@ function supportScale(full: number, direction: Direction, gain: number): (x: num
return (x) => improvement(x, full, direction) / gain;
}
/** 消融模型在引擎眼里的样子:收益刻度是撑几成,没有"都不走"。 */
export function ablationView(model: AblationModel): ValueModel {
return {
weights: model.weights,
rawKnots: model.rawKnots,
scaledKnots: model.supportKnots,
expected: model.expected,
prior: model.prior,
space: model.space,
floor: null,
scale: supportScale(model.full, model.direction, model.gain),
};
}
/**
* 成分模型(6.7"成分与权重"):每条解释一个成分,开放预留一个成分;每个成分对每一格有一个分布,
* 解释用它自己的三个数补尾巴,开放预留用这一格的均匀分布(6.3"消融")。同一成分内各格互相独立。
@@ -144,26 +189,33 @@ export function cellGrid(knots: readonly Knots[], k: number): { mids: number[];
}
/**
* 模型里第 c 格的网格(6.7"只测一部分格"),测 k 格时用。在分数原值上取折点、切小段:方法说"每格在自己的开放预留范围内
* 模型里第 c 项的网格(6.7"只测一部分格"),测 k 项时用。在分数原值上取折点、切小段:方法说"每格在自己的开放预留范围内
* 按折点切段",开放预留和折点都是在分数原值上定的(尾巴按 q1 − (q5 − q1)、q9 + (q9 − q5),预留按 L − W/2、U + W/2
* 的浮点原式),去重只合并浮点上完全相等的值。先换到撑几成刻度再去重,相差一个末位的两个原值折点可能换算后变成同一个数,
* 段数和 s 就会跟原值上的不一样。切好后只把小段中点换算成撑几成;把握与刻度无关。
* 的浮点原式),去重只合并浮点上完全相等的值。先换到收益刻度再去重,相差一个末位的两个原值折点可能换算后变成同一个数,
* 段数和 s 就会跟原值上的不一样。切好后只把小段中点换算成收益刻度;把握与刻度无关。
*/
export function measuredGrid(model: AblationModel, c: number, k: number): { mids: number[]; mass: number[][] } {
export function gridOf(model: ValueModel, c: number, k: number): { mids: number[]; mass: number[][] } {
const { mids, mass } = cellGrid(model.rawKnots[c]!, k);
return { mids: mids.map(supportScale(model.full, model.direction, model.gain)), mass };
return { mids: mids.map(model.scale), mass };
}
/** 消融模型里第 c 格的网格,中点换算成撑几成(6.7"只测一部分格")。 */
export function measuredGrid(model: AblationModel, c: number, k: number): { mids: number[]; mass: number[][] } {
return gridOf(ablationView(model), c, k);
}
/**
* 只测 S 里的格时的 E_{S 的结果}[max_p E(撑_p | S 的结果)](6.7"信息价值"),在撑几成刻度上。
* 对测到的格的全部小段组合求和:成分 j 的联合把握 = 起始权重 × 各测到格在所选小段的把握;它们的和 marg 是这个组合的把握,
* 按它归一就是更新后的权重。网格见 measuredGrid。测到的格取小段中点(已换成撑几成),没测的格用更新后的权重平均各成分的期望,取各因子里最大的。
* 只测 S 里的项时的 E_{S 的结果}[max(floor, max_p E(收益_p | S 的结果))](6.7"信息价值"、6.12),在收益刻度上,
* 没有"都不走"(floor 为 null)时不和 floor 取大。
* 对测到的项的全部小段组合求和:成分 j 的联合把握 = 起始权重 × 各测到项在所选小段的把握;它们的和 marg 是这个组合的把握,
* 按它归一就是更新后的权重。网格见 gridOf。测到的项取小段中点(已换成收益刻度),没测的项用更新后的权重平均各成分的期望,取各项里最大的。
*/
function expectedBestGrid(model: AblationModel, S: readonly number[]): number {
function expectedBestGrid(model: ValueModel, S: readonly number[]): number {
const k = S.length;
const J = model.weights.length;
const grids = S.map((c) => measuredGrid(model, c, k));
const rest = model.factors.map((_, c) => c).filter((c) => !S.includes(c));
const floor = model.floor ?? -Infinity;
const grids = S.map((c) => gridOf(model, c, k));
const rest = model.prior.map((_, c) => c).filter((c) => !S.includes(c));
const partial = Array.from({ length: k + 1 }, () => new Float64Array(J));
for (let j = 0; j < J; j++) partial[0]![j] = model.weights[j]!;
let total = 0;
@@ -173,7 +225,7 @@ function expectedBestGrid(model: AblationModel, S: readonly number[]): number {
let marg = 0;
for (let j = 0; j < J; j++) marg += cur[j]!;
if (marg === 0) return;
let best = seen;
let best = Math.max(seen, floor);
for (const c of rest) {
const e = model.expected[c]!;
let num = 0;
@@ -195,36 +247,80 @@ function expectedBestGrid(model: AblationModel, S: readonly number[]): number {
}
/**
* 全部格都测时的 E[max_p 撑_p](6.7"信息价值"):同一成分内"各格撑几成的最大值低于 t"的把握 = 各格把握连乘,
* 期望 = 下端 + ∫(1 − 连乘) dt,逐段多项式精确积分;再按起始权重相加。
* 全部项都测时的 E[max(floor, max_p 收益_p)](6.7"信息价值"、6.12):同一成分内"各项最大值低于 t"的把握 = 各项把握连乘,
* 期望 = 下端 + ∫(1 − 连乘) dt,逐段多项式精确积分;再按起始权重相加。下端:有"都不走"时是 floor(t 低于它时
* max(floor, ·) 一定不低于 t,被积函数是 1),没有时是各项折线的最低点。
*/
function expectedBestAll(model: AblationModel): number {
function expectedBestAll(model: ValueModel): number {
let v = 0;
for (let j = 0; j < model.weights.length; j++) {
const lines = model.supportKnots.map((cell) => cell[j]!);
const lo = Math.min(...lines.map((k) => k[0]![0]));
const lines = model.scaledKnots.map((cell) => cell[j]!);
const lo = model.floor ?? Math.min(...lines.map((k) => k[0]![0]));
v += model.weights[j]! * (lo + integrateUpperTail(lines.map((knots) => [{ weight: 1, knots }]), lo));
}
return v;
}
/** max(floor, max_p E(收益_p)):不测时能拿到的最好期望,在收益刻度上。 */
function bestPrior(model: ValueModel): number {
return Math.max(model.floor ?? -Infinity, ...model.prior);
}
/** 用网格算的信息价值(不论测几项都走网格),S 是项的下标。 */
export function valueByGrid(model: ValueModel, S: readonly number[]): number {
return model.space * (expectedBestGrid(model, [...S].sort((a, b) => a - b)) - bestPrior(model));
}
/** 全部项都测时的信息价值(一维精确积分)。 */
export function valueExact(model: ValueModel): number {
return model.space * (expectedBestAll(model) - bestPrior(model));
}
/**
* 信息价值 VOI(S) = E_{S 的结果}[max(floor, max_p E(收益_p | S 的结果))] − max(floor, max_p E(收益_p))(6.7、6.12),
* S 是项的下标;全部项都测走一维精确积分,只测一部分走网格,空集是 0。不截断。
*/
export function valueOf(model: ValueModel, S: readonly number[]): number {
if (S.length === 0) return 0;
return S.length === model.prior.length ? valueExact(model) : valueByGrid(model, S);
}
/** 网格算法算的信息价值,S 是格的下标(不论测几格都走网格;用来和精确算法互相对照)。 */
export function informationValueGrid(model: AblationModel, S: readonly number[]): number {
return model.space * (expectedBestGrid(model, [...S].sort((a, b) => a - b)) - Math.max(...model.prior));
return valueByGrid(ablationView(model), S);
}
/** 全部格都测时的信息价值(一维精确积分)。 */
export function informationValueExact(model: AblationModel): number {
return model.space * (expectedBestAll(model) - Math.max(...model.prior));
return valueExact(ablationView(model));
}
/** 消融的信息价值 VOI(S)(6.7),S 是格的下标。 */
export function informationValue(model: AblationModel, S: readonly number[]): number {
return valueOf(ablationView(model), S);
}
/**
* 信息价值 VOI(S) = E_{S 的结果}[max_p E(收益_p | S 的结果)] − max_p E(收益_p)(6.7),S 是格的下标;
* 全部格都测走一维精确积分,只测一部分走网格,空集是 0。不截断。
* 成分新权重(6.8、6.12):起始权重 × 各有效项(scores 里不是 null 的)上这个成分在实测处的密度(分数原值上,连乘),
* 再归一;实测落在所有成分之外、权重之和为 0 时保持起始权重。scores 与项的下标对齐,没有有效实测的项为 null。
*/
export function informationValue(model: AblationModel, S: readonly number[]): number {
if (S.length === 0) return 0;
return S.length === model.factors.length ? informationValueExact(model) : informationValueGrid(model, S);
export function updatedWeights(
model: ValueModel,
scores: ReadonlyArray<number | null>,
): { weights: number[]; kept: boolean } {
const raw = model.weights.map((w, j) =>
scores.reduce<number>((v, x, c) => (x === null ? v : v * density(model.rawKnots[c]![j]!, x)), w),
);
const sum = raw.reduce((t, v) => t + v, 0);
const kept = sum === 0;
return { weights: kept ? [...model.weights] : raw.map((v) => v / sum), kept };
}
/** 第 c 项的方向收益(6.8、6.12):有有效实测 x 就用实测换算,否则用 weights 对各成分的期望加权平均。 */
export function itemGain(model: ValueModel, c: number, weights: readonly number[], x: number | null): number {
return x !== null
? model.space * model.scale(x)
: model.space * model.expected[c]!.reduce((t, e, j) => t + weights[j]! * e, 0);
}
/** 一个测格组合的价值与成本(6.7"价值减成本")。 */
@@ -253,6 +349,19 @@ export interface ComboParams {
decay: number;
}
/** m′ = 1 + r + … + r^(R̂−1),R̂ = 0 时是 0(6.7"价值减成本");消融和诊断共用。 */
export function usePotential(rHat: number, decay: number): number {
let mPrime = 0;
for (let j = 0; j < rHat; j++) mPrime += decay ** j;
return mPrime;
}
/** 成本 = 2 max(0, ĝ₀) × [(K + n)/(K + n + 1) − K/(K + 1)],n 是这次占用的运行次数(6.7、6.12);消融和诊断共用。 */
export function runCost(g0: number, batchCap: number, runs: number): number {
const K = batchCap;
return 2 * Math.max(0, g0) * ((K + runs) / (K + runs + 1) - K / (K + 1));
}
/**
* 列出格数 1..min(格数, 余额 − 1) 的全部非空组合并算价值、成本、净值(6.7"选格"第 1 条)。余额为 0 或 1 时一个都没有。
* 组合按格数从少到多、同格数按因子编号字典序列出。ĝ₀ = h × max_p 期望撑几成,原值另给,成本里取 max(0, ĝ₀)。
@@ -265,10 +374,9 @@ export function evaluateCombos(model: AblationModel, params: ComboParams): { g0:
for (const S of combinations(model.factors.length, size)) {
const voi = informationValue(model, S);
const rHat = Math.ceil((params.balance - size) / K);
let mPrime = 0;
for (let j = 0; j < rHat; j++) mPrime += params.decay ** j;
const mPrime = usePotential(rHat, params.decay);
const value = params.u * mPrime * voi;
const cost = 2 * Math.max(0, g0) * ((K + size) / (K + size + 1) - K / (K + 1));
const cost = runCost(g0, K, size);
combos.push({ factors: S.map((c) => model.factors[c]!), voi, rHat, mPrime, value, cost, net: value - cost });
}
}