feat: 诊断(任务书 18 第一块):诊断数学、四种提交、一次运行跑多个实验、账本与材料、flow 诊断各步
消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。 eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+139
-31
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user