feat: 研究规则 rules 模块(任务书 02)
rules 是纯计算,src/rules 共 1761 行:方法.md 第六部分的全部公式、第七部分的余额与扣次、4.2 的入口分配,以及种子与规则登记表。 预期值有三个来源:方法.md 的手算例;tests/fixtures/rules/ 下不看实现写的 Python 脚本;参考项目 calc15、calc14b 里与现行方法一致的部分。审阅做了两轮,每轮都换一个全新上下文的子 Agent,提出的问题都已修复或写明不修的理由。 用户确认的约定(原话"1–8 都同意"):入池、采纳、消融中发现都用严格不等号;成本用 max(0, ĝ₀);没测的格附"含它的最好组合"的净值;战绩在分数原值上判。三个数改为严格递增后,删掉了尖峰的全部专门处理(bfb96ba)。vocab 里 Range 的说明同步改为严格递增。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
import type { Direction } from "../vocab/index.ts";
|
||||
import type { AblationExplanation } from "./ablation.ts";
|
||||
import { totalGain } from "./ablation.ts";
|
||||
import {
|
||||
combinations,
|
||||
compareIds,
|
||||
epsilonFloor,
|
||||
GRID_COMBINATION_CAP,
|
||||
GRID_SUBDIVISIONS,
|
||||
improvement,
|
||||
} from "./basics.ts";
|
||||
import { ablationCellMixture, below, 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)。
|
||||
|
||||
/** 开放预留成分的名字(成分名单里排在最后)。 */
|
||||
export const OPEN_RESERVE = "openReserve";
|
||||
|
||||
/** 建成分模型要的输入(6.7)。baseline 是本批基线 b,full 是被采纳的完整代码的分数。 */
|
||||
export interface AblationModelInput {
|
||||
baseline: number;
|
||||
full: number;
|
||||
direction: Direction;
|
||||
knownBest: number | null;
|
||||
epsilon: number;
|
||||
lambda: number;
|
||||
/** 被采纳改法的因子编号。 */
|
||||
factors: readonly string[];
|
||||
/** 每条解释对每一格都给了三个数。 */
|
||||
explanations: readonly AblationExplanation[];
|
||||
}
|
||||
|
||||
export interface AblationModel {
|
||||
/** 因子编号,按编号排;下文"格的下标"都按这个次序。 */
|
||||
factors: string[];
|
||||
/** 成分:各条解释的编号(按编号排),最后是开放预留。 */
|
||||
components: string[];
|
||||
/** 起始权重(6.7"成分与权重"):解释合起来 1 − λ(研究者等权、同一人平分),开放预留 λ。 */
|
||||
weights: number[];
|
||||
/** 被采纳的完整代码的分数 f(完整) 和方向:分数原值换算成撑几成要用。 */
|
||||
full: number;
|
||||
direction: Direction;
|
||||
/** 总提升 G。 */
|
||||
gain: number;
|
||||
/** 剩余空间 h。 */
|
||||
space: number;
|
||||
/** rawKnots[格][成分]:分数原值上的折线(开放预留是这一格自己的均匀段)。 */
|
||||
rawKnots: Knots[][];
|
||||
/** supportKnots[格][成分]:撑几成刻度上的折线。 */
|
||||
supportKnots: Knots[][];
|
||||
/** expected[格][成分]:这个成分下这一格撑几成的期望。 */
|
||||
expected: number[][];
|
||||
/** prior[格]:按起始权重平均的期望撑几成;乘 h 就是 E(收益_p)。 */
|
||||
prior: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 剩余空间 h(6.7"方向收益"):有已知最好时 max(K* − f(完整), ε/100)(按方向折算),没有时 1。
|
||||
*/
|
||||
export function remainingSpace(full: number, knownBest: number | null, epsilon: number, direction: Direction): number {
|
||||
return knownBest === null ? 1 : Math.max(improvement(full, knownBest, direction), epsilonFloor(epsilon));
|
||||
}
|
||||
|
||||
/** 分数原值 x → 撑几成 [f(完整) − x] ÷ G(按方向折算)。 */
|
||||
function supportScale(full: number, direction: Direction, gain: number): (x: number) => number {
|
||||
return (x) => improvement(x, full, direction) / gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成分模型(6.7"成分与权重"):每条解释一个成分,开放预留一个成分;每个成分对每一格有一个分布,
|
||||
* 解释用它自己的三个数补尾巴,开放预留用这一格的均匀分布(6.3"消融")。同一成分内各格互相独立。
|
||||
*/
|
||||
export function ablationModel(input: AblationModelInput): AblationModel {
|
||||
const gain = totalGain(input.baseline, input.full, input.direction);
|
||||
const factors = [...input.factors].sort(compareIds);
|
||||
const explanations = [...input.explanations].sort((a, b) => compareIds(a.id, b.id));
|
||||
const mixtures = factors.map((f) =>
|
||||
ablationCellMixture(
|
||||
explanations.map((e) => ({ id: e.id, author: e.author, range: e.cells.find((c) => c.factor === f)!.range })),
|
||||
input.lambda,
|
||||
),
|
||||
);
|
||||
const toSupport = supportScale(input.full, input.direction, gain);
|
||||
const rawKnots = mixtures.map((m) => m.map((c) => c.knots));
|
||||
const supportKnots = rawKnots.map((cell) => cell.map((k) => mapKnots(k, toSupport, input.direction === "maximize")));
|
||||
const weights = mixtures[0]!.map((c) => c.weight);
|
||||
const expected = supportKnots.map((cell) => cell.map(knotsMean));
|
||||
return {
|
||||
factors,
|
||||
components: [...explanations.map((e) => e.id), OPEN_RESERVE],
|
||||
weights,
|
||||
full: input.full,
|
||||
direction: input.direction,
|
||||
gain,
|
||||
space: remainingSpace(input.full, input.knownBest, input.epsilon, input.direction),
|
||||
rawKnots,
|
||||
supportKnots,
|
||||
expected,
|
||||
prior: expected.map((row) => row.reduce((s, e, j) => s + weights[j]! * e, 0)),
|
||||
};
|
||||
}
|
||||
|
||||
/** 测 k 格时每格的小段数 m:满足 m^k ≤ 4,000,000 的最大整数(6.7"信息价值"),用整数运算核对。 */
|
||||
export function gridSize(k: number): number {
|
||||
let m = Math.floor(GRID_COMBINATION_CAP ** (1 / k));
|
||||
while (m ** k > GRID_COMBINATION_CAP) m -= 1;
|
||||
while ((m + 1) ** k <= GRID_COMBINATION_CAP) m += 1;
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一格的网格(6.7"只测一部分格"):取这一格全部成分的折点(含开放预留两端)去重排序,段数 = 折点数 − 1;
|
||||
* s = min(20, ⌊m ÷ 段数⌋),s ≥ 1 时每段等分 s 份,段数超过 m 时整格等宽切 m 份。
|
||||
* 每个小段的代表点是中点;成分 j 在小段 [左, 右] 的把握 = F_j(右) − F_j(左),各小段把握加起来是 1。
|
||||
*/
|
||||
export function cellGrid(knots: readonly Knots[], k: number): { mids: number[]; mass: number[][] } {
|
||||
const brk = [...new Set(knots.flatMap((kk) => kk.map(([x]) => x)))].sort((a, b) => a - b);
|
||||
const segs = brk.length - 1;
|
||||
const m = gridSize(k);
|
||||
const s = Math.min(GRID_SUBDIVISIONS, Math.floor(m / segs));
|
||||
const edges: number[] = [];
|
||||
if (s >= 1) {
|
||||
for (let i = 0; i < segs; i++) {
|
||||
const a = brk[i]!;
|
||||
const step = (brk[i + 1]! - a) / s;
|
||||
for (let j = 0; j < s; j++) edges.push(a + j * step);
|
||||
}
|
||||
} else {
|
||||
const step = (brk[segs]! - brk[0]!) / m;
|
||||
for (let j = 0; j < m; j++) edges.push(brk[0]! + j * step);
|
||||
}
|
||||
edges.push(brk[segs]!);
|
||||
const mids: number[] = [];
|
||||
for (let i = 0; i + 1 < edges.length; i++) mids.push((edges[i]! + edges[i + 1]!) / 2);
|
||||
const mass = knots.map((kk) => {
|
||||
const cdf = edges.map((x) => below(kk, x));
|
||||
return mids.map((_, i) => cdf[i + 1]! - cdf[i]!);
|
||||
});
|
||||
return { mids, mass };
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型里第 c 格的网格(6.7"只测一部分格"),测 k 格时用。在分数原值上取折点、切小段:方法说"每格在自己的开放预留范围内
|
||||
* 按折点切段",开放预留和折点都是在分数原值上定的(尾巴按 q1 − (q5 − q1)、q9 + (q9 − q5),预留按 L − W/2、U + W/2
|
||||
* 的浮点原式),去重只合并浮点上完全相等的值。先换到撑几成刻度再去重,相差一个末位的两个原值折点可能换算后变成同一个数,
|
||||
* 段数和 s 就会跟原值上的不一样。切好后只把小段中点换算成撑几成;把握与刻度无关。
|
||||
*/
|
||||
export function measuredGrid(model: AblationModel, 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 只测 S 里的格时的 E_{S 的结果}[max_p E(撑_p | S 的结果)](6.7"信息价值"),在撑几成刻度上。
|
||||
* 对测到的格的全部小段组合求和:成分 j 的联合把握 = 起始权重 × 各测到格在所选小段的把握;它们的和 marg 是这个组合的把握,
|
||||
* 按它归一就是更新后的权重。网格见 measuredGrid。测到的格取小段中点(已换成撑几成),没测的格用更新后的权重平均各成分的期望,取各因子里最大的。
|
||||
*/
|
||||
function expectedBestGrid(model: AblationModel, 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 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;
|
||||
const walk = (level: number, seen: number): void => {
|
||||
const cur = partial[level]!;
|
||||
if (level === k) {
|
||||
let marg = 0;
|
||||
for (let j = 0; j < J; j++) marg += cur[j]!;
|
||||
if (marg === 0) return;
|
||||
let best = seen;
|
||||
for (const c of rest) {
|
||||
const e = model.expected[c]!;
|
||||
let num = 0;
|
||||
for (let j = 0; j < J; j++) num += cur[j]! * e[j]!;
|
||||
if (num / marg > best) best = num / marg;
|
||||
}
|
||||
total += best * marg;
|
||||
return;
|
||||
}
|
||||
const g = grids[level]!;
|
||||
const next = partial[level + 1]!;
|
||||
for (let i = 0; i < g.mids.length; i++) {
|
||||
for (let j = 0; j < J; j++) next[j] = cur[j]! * g.mass[j]![i]!;
|
||||
walk(level + 1, Math.max(seen, g.mids[i]!));
|
||||
}
|
||||
};
|
||||
walk(0, -Infinity);
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部格都测时的 E[max_p 撑_p](6.7"信息价值"):同一成分内"各格撑几成的最大值低于 t"的把握 = 各格把握连乘,
|
||||
* 期望 = 下端 + ∫(1 − 连乘) dt,逐段多项式精确积分;再按起始权重相加。
|
||||
*/
|
||||
function expectedBestAll(model: AblationModel): 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]));
|
||||
v += model.weights[j]! * (lo + integrateUpperTail(lines.map((knots) => [{ weight: 1, knots }]), lo));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/** 网格算法算的信息价值,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));
|
||||
}
|
||||
|
||||
/** 全部格都测时的信息价值(一维精确积分)。 */
|
||||
export function informationValueExact(model: AblationModel): number {
|
||||
return model.space * (expectedBestAll(model) - Math.max(...model.prior));
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息价值 VOI(S) = E_{S 的结果}[max_p E(收益_p | S 的结果)] − max_p E(收益_p)(6.7),S 是格的下标;
|
||||
* 全部格都测走一维精确积分,只测一部分走网格,空集是 0。不截断。
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/** 一个测格组合的价值与成本(6.7"价值减成本")。 */
|
||||
export interface ComboValue {
|
||||
/** 组合里的因子,按编号排。 */
|
||||
factors: string[];
|
||||
voi: number;
|
||||
/** R̂ = ⌈(余额 − |S|) ÷ 每批上限⌉。 */
|
||||
rHat: number;
|
||||
/** m′ = 1 + r + … + r^(R̂−1),R̂ = 0 时是 0。 */
|
||||
mPrime: number;
|
||||
/** 价值 = u × m′ × VOI。 */
|
||||
value: number;
|
||||
/** 成本 = 2 max(0, ĝ₀) × [(K + |S|)/(K + |S| + 1) − K/(K + 1)]。 */
|
||||
cost: number;
|
||||
/** 净值 = 价值 − 成本。 */
|
||||
net: number;
|
||||
}
|
||||
|
||||
export interface ComboParams {
|
||||
balance: number;
|
||||
batchCap: number;
|
||||
/** 测出的知识会被用上的概率 u。 */
|
||||
u: number;
|
||||
/** 衰减 r。 */
|
||||
decay: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出格数 1..min(格数, 余额 − 1) 的全部非空组合并算价值、成本、净值(6.7"选格"第 1 条)。余额为 0 或 1 时一个都没有。
|
||||
* 组合按格数从少到多、同格数按因子编号字典序列出。ĝ₀ = h × max_p 期望撑几成,原值另给,成本里取 max(0, ĝ₀)。
|
||||
*/
|
||||
export function evaluateCombos(model: AblationModel, params: ComboParams): { g0: number; combos: ComboValue[] } {
|
||||
const g0 = model.space * Math.max(...model.prior);
|
||||
const K = params.batchCap;
|
||||
const combos: ComboValue[] = [];
|
||||
for (let size = 1; size <= Math.min(model.factors.length, params.balance - 1); size++) {
|
||||
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 value = params.u * mPrime * voi;
|
||||
const cost = 2 * Math.max(0, g0) * ((K + size) / (K + size + 1) - K / (K + 1));
|
||||
combos.push({ factors: S.map((c) => model.factors[c]!), voi, rHat, mPrime, value, cost, net: value - cost });
|
||||
}
|
||||
}
|
||||
return { g0, combos };
|
||||
}
|
||||
Reference in New Issue
Block a user