总览 · ← 返回运行 20261003-153851-search-t1-D-smoke2h
节点 n7
OT-CFM 最小版:两输入视图在共享 30 维 PCA 上用 minibatch 精确 OT 配对训练条件流匹配速度场 v(z,t=1),从最新阶段的输出细胞按阻尼步长 τ=α·r 做 Euler 前推,位移解码回 HVG 并加到细胞自己的表达上;单输入视图退化为均匀抽样照抄。
| 运行?一次完整的自动搜索或 Agent 会话,有自己的锁定配置和证据包。 | 20261003-153851-search-t1-D-smoke2h |
|---|---|
| 父节点 | (种子,没有父节点) |
| 子节点 | — |
| 操作?种子:人写的起点;改进:在父节点上改;草稿:从头写;修复:修父节点的报错。 | 草稿 |
| 状态 | 程序报错 |
| 分数 | proxy10 52.75 |
| 审查 | 未审查 |
| 用时?从运行开始到结束(或到现在)的挂钟时间。 | 24 分 |
| 程序版本 | 34b180229963fe2613eff448b3f9dcc92e097ce1 (programs.git) |
| 备注 | crash on X3: home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/exec_X3/src/solution/run.py", line 171, in main log(f"train done loss {losses[-1]:.4f} cap_norm={cap_… |
方法说明?节点程序自带的 METHOD.md:这个程序做了什么、为什么。
来自 programs.git 34b1802299:solution/METHOD.md
OT-CFM 最小版:两输入视图在共享 30 维 PCA 上用 minibatch 精确 OT 配对训练条件流匹配速度场 v(z,t=1),从最新阶段的输出细胞按阻尼步长 τ=α·r 做 Euler 前推,位移解码回 HVG 并加到细胞自己的表达上;单输入视图退化为均匀抽样照抄。
方法(family_id: ot_cfm)
- 前处理:
view_io.inputs_by_time(manifest)取全部输入阶段,只用相对时间:dt_in = t_last − t_prev、dt_out = t_target − t_last、r = dt_out/dt_in。不读视图路径 / 名称 / 池大小 / 绝对时间,因此伪装视图(时间整体平移)预测不变。表达按视图契约已是 log1p(CP10k),不再重算。 - HVG:两阶段
covered_mask的交集内,按两阶段逐基因方差的均值取前 2,500 个(方差由稀疏矩阵E[x²]−E[x]²现场算,不稠密化全矩阵)。 - 联合 PCA:两阶段各随机取 ≤4,000 细胞,减共同均值后
np.linalg.svd,取前 30 个载荷 W;z 再除以全局标量s = sqrt(mean(z²))。 - 训练(OT-CFM,arXiv:2302.00482 / arXiv:2210.02747):batch 256,batch 内用
scipy.optimize.linear_sum_assignment对平方欧氏代价做精确 OT 配对(等价于 torchcfm 的ExactOptimalTransportConditionalFlowMatcher的耦合;本机 torchcfm 版本 API 不同,故直接用 scipy 实现同一配对,确定性);x_t=(1−t)x_0+t x_1+σε(σ=0.05),目标速度u_t=x_1−x_0;v 为 MLP 31→256→256→256→30,SiLU,Adam lr 1e-3,4,000 步,时间上限 240 s,纯 CPU(EXECUTION.json{"gpu": false})。 - 前推:t 固定为 1(只用训练区间端点的局部速度,不向 t>1 外推)。τ=α·r,α 默认 0.5,Euler K=8 步;每步位移范数上限 = 训练配对位移范数的 90% 分位(scaled 单位),总位移同样受此上限约束,防过冲。
- 解码:
Δx = (Δz·s)·Wᵀ只作用于 2,500 个 HVG,加到该细胞自己的表达上(残差与型内共变保留)。非零元:max(0, x+Δx);零元只在Δx > θ时开启(θ = 全部正 Δx 的 95% 分位)。非 HVG 基因原样不动。 - 输出细胞:从最新阶段均匀抽样,
n_out = view_io.target_n_cells(manifest, n_last),组成不变。 - 单输入退路:只有一个输入阶段时无法学速度场,输出 = 最新阶段均匀抽样照抄(生长重采样关闭)。该分支只由
len(inputs)决定。 --ablate:mechanism/velocity0/velocity/zero/copy_last→ Δz=0(同流程 copy_last);random→ 保留每细胞位移范数、方向在 PCA 内均匀随机;per_type_mean→ 每细胞速度换成本型 v 的均值(步长、上限、解码、θ 全同)。
知识来源
- arXiv:2302.00482(Tong 等,OT-CFM:minibatch 精确 OT 配对 + 条件流匹配);arXiv:2210.02747(Lipman 等,Flow Matching:直线路径与常速目标)。二者均为方法学文献,不含任何保留阶段测量值。
- 未使用 T1 发育事件表中的任何一行(本 draft 不生成新状态、不命名类型、不用标记程序),也未使用
external/、prior/。未使用uns.celltype_palette。程序不含任何硬编码的阶段统计量、类型名单、比例或细胞数。
验证过什么
vec-check通过:X3 视图(两输入,E8.75+E9.0 → E9.5)与 proxy10 视图(单输入 E8.5 → E9.5)。- X3 视图整流程约 90 s(4,000 步),proxy10 单输入分支约 2 s,内存远低于 28 GB。
- 训练损失从 0.260 降到 0.171(500 步冒烟 → 4,000 步),说明速度场确实拟合了 OT 配对方向;
cap_norm(训练配对位移 90% 分位,scaled)≈3.46,α=0.5 时平均位移 |Δz·s| ≈8.4(log 表达单位,HVG 上),上限基本不触发过冲。 - 查分(A 半):
proxy10,单输入照抄分支:52.97(de_score 12.87 / de_direction 14.67 / mmd_u 15.50 / variogram 9.93;raw mmd_u 0.0406,raw variogram 0.0012)。与节点 5 的 proxy10 52.94 同一水平 —— 单输入分支就是照抄,符合预期,不含新信息。X3,α=0.5、θ=95% 分位开启零元:50.14(de_score raw −0.1857 → 11.14 / de_direction raw −0.0173 → 12.34 / mmd_u raw 0.02667 → 17.66(skill 0.589,高于地板)/ variogram raw 0.001834 → 9.01(低于地板 10))。
- 结论(诚实):前向 OT-CFM 场在 X3 上确实改善了细胞状态分布(mmd_u 0.02667,skill 0.589 > 0.5,比地板好),方向上 DE 两项在地板附近,
variogram明显被损害(0.001834 vs 地板对应值)。总分 50.14 低于当前最佳节点 5 的 X3 55.32,没有超过本 run 的最好节点。损害 variogram 的嫌疑是解码时对 2,500 个 HVG 做稠密加法并开启零元(nnz 1,282,705 → 1,292,940),破坏了基因间共变结构。
没验证什么 / 待后续节点
- 因时间预算(本节点会话剩余额度用尽),只查到 α=0.5 主配置和 proxy10 的分;
velocity=0同流程对照、random、per_type_mean三个对照,以及 α∈{0.25,1.0}、θ∈{90,99, 不开启零元}、多 seed 复跑都未完成查分,所以「机制相对同流程 copy_last 的净收益」尚未由查分证明,只能记为 unclear / 待测。--ablate接口本身已实现(三个名字均可运行并改变输出)。 - 最有价值的下一步:(1) 关掉零元开启(θ→∞)并减小 HVG 数 / 只解码位移最大的少数基因,看 variogram 是否回到地板以上 —— 这是本方案当前唯一明确为负的组分;(2) 把 α 与 r 的关系改成向 0 收缩(α≈0.25)并跑 v=0 对照,确认 mmd_u 的收益是真的;(3) 与节点 5 的后向祖先位移机制不同源,若 (1)(2) 后 mmd_u 收益保住而 variogram 不再受损,可与节点 5 的 addnz 解码 / 型内选择组合。
调研员的计划
| 名称 | OT-CFM 最小版:PCA 中 minibatch-OT 配对学状态相关速度场,从最新阶段受限步长前推,加回细胞残差 |
|---|---|
| 动机 | ot_cfm 族在本 run 还没有已完成的实现,节点 7 是该族第一个 draft。当前最佳是节点 5(local_ot 系,rank3 54.31)。它在 X3 上的收益几乎全部来自细胞状态:X3 的 mmd_u 从 0.03281 降到 0.02075,cell_state 从 49.21 升到 62.23。但 DE 没有学到:X3 de_score 为 -0.0649,低于地板;X3 de_direction 只有 0.0159。节点 5 的位移来自后向的祖先重心(输出细胞减去它在 E8.5 的祖先均值),而且在 30 个 PCA 模块上放大 3 倍外推,存在过冲风险。组成系(节点 3、4)在 X3 上都在地板附近(48.7)。ot_cfm 的不同之处是学一个平滑、随细胞状态变化的前向速度场 v(x),在最新阶段每个细胞自己的位置上取值,并用受限步长积分。这样检验“局部前向方向”能否比节点 5 的后向位移给出更好的方向(direction / de_recovery),同时保住 cell_state。X3 权重为 2,是主要信号;proxy10 只有单输入,只能照抄(节点 5 在 proxy10 上为 52.94)。 |
| 做法 | 0)共用前处理。读视图中全部输入阶段,用 view_io.inputs_by_time 按时间排序。时间只用相对差:dt_in = t_last − t_prev,dt_out = t_target − t_last,r = dt_out/dt_in。伪装视图把所有时间平移 1 天,r 不变,所以预测必须与真实视图完全一致。不读任何尺子名、路径或池子大小。表达按视图契约做 log1p 归一化。在两个输入阶段的并集上选 2,500 个 HVG,在两阶段等量子样本上拟合联合 PCA(30 维,载荷 W)。 1)单输入退路。视图只有一个输入阶段时(proxy10),速度场无法训练,输出为从最新阶段均匀抽样的照抄,与节点 5 的单输入分支相同,生长重采样关闭。这个分支只由输入阶段数决定,与视图身份无关。final(官方 E8.5、E9.5,间隔 1 天,全胚,r=1)和 X3 都走两阶段分支。若某视图两阶段的基因集合不一致,缺失基因用另一阶段该基因的均值补齐,并且不进 HVG。 2)配对与训练。每个阶段随机取 4,000 个细胞(seed 控制),作为 x0(前一阶段)和 x1(最新阶段)的 PCA 坐标,按 PCA 全局标准差统一缩放。用 torchcfm 的 ExactOptimalTransportConditionalFlowMatcher(sigma=0.05),batch 256,在 batch 内做精确 OT 配对。只比较这一种配对方式,不做 SB 变体。速度场 v(x,t) 用 MLP:输入 31 维(30 维 PCA + t),3 层隐藏层,每层 256,SiLU 激活。Adam,学习率 1e-3,训练 4,000 步,设 5 分钟时间上限,在 GPU 或 CPU 上跑。先用 1,000 细胞、500 步做冒烟测试,再上全量。 3)前推,在端点做局部延续。t 固定为 1,即只用训练区间端点处的局部速度,不向训练范围外的 t 外推。从最新阶段的全部输出细胞 z_i 出发,用 Euler 法积分 K=8 步,总时长 τ = α·r。α 初值 0.5,搜索 {0.25, 0.5, 0.75, 1.0}。对每个细胞的总位移范数设上限:不超过 α·r 乘以训练配对位移范数的 90% 分位数,防止过冲。 4)解码,加回残差,并处理稀疏性。Δz_i = z_i(τ) − z_i;Δx_i = Δz_i·Wᵀ,只作用于 HVG,再乘回 PCA 缩放。输出表达 = 该细胞自己的原表达 + Δx_i,因此细胞残差和型内共变都完整保留。非零元直接相加后 clip≥0。零元只有当 Δx_ig > θ 时才开启(取值为 Δx_ig):θ 取全部正 Δx 的 95% 分位数,搜索 {90, 95, 99},也可以不开启。这避免了稠密小正值,同时允许少量基因由关变开。非 HV… |
| 风险 | (1) 两个边际不能唯一确定动力学:场可能主要学到扩散,或者学到从 E8.5 分布到 E9.5 分布的“平均化”方向。早期信号:输出在 PCA 上的总方差相对输入明显变大,或者 mmd_u 变好而 DE 两项相对 v=0 不变或变差。(2) 过冲:α=1 时 mmd_u 和 variogram 恶化,零值比例明显下降。监控输出零值比例和每基因方差相对输入的变化。(3) 速度退化为每型常向量:用 mechanism_evidence 中的型内 R² 检查;若 R²>0.9,且与“每型均值速度”对照分差在噪声内,就如实写成该机制没有贡献状态依赖性。(4) 与节点 5 机制相似(都用 OT 配对),收益可能重合。若不超过节点 5,如实报告;这只是 draft,不靠组合冲分。(5) 训练不稳或超时:先跑冒烟测试,设训练时间上限,损失曲线要下降并趋于平稳。(6) 零元开启阈值太低会变成稠密加法,损害 variogram。对比 θ 档位时同时看 variogram 原始值。 |
代码改动?这个节点的程序和父节点程序的逐行差别:绿色是新增,红色是删除。
对比:这个提交的上一版(种子程序:相对空仓库)。改动的文件:solution/EXECUTION.json +1 −0、solution/METHOD.md +33 −0、solution/run.py +249 −0
diff --git a/solution/EXECUTION.json b/solution/EXECUTION.jsonnew file mode 100644index 0000000..9d5125c--- /dev/null+++ b/solution/EXECUTION.json@@ -0,0 +1 @@+{"gpu": false}diff --git a/solution/METHOD.md b/solution/METHOD.mdnew file mode 100644index 0000000..4847a57--- /dev/null+++ b/solution/METHOD.md@@ -0,0 +1,33 @@+OT-CFM 最小版:两输入视图在共享 30 维 PCA 上用 minibatch 精确 OT 配对训练条件流匹配速度场 v(z,t=1),从最新阶段的输出细胞按阻尼步长 τ=α·r 做 Euler 前推,位移解码回 HVG 并加到细胞自己的表达上;单输入视图退化为均匀抽样照抄。++## 方法(family_id: ot_cfm)++- 前处理:`view_io.inputs_by_time(manifest)` 取全部输入阶段,只用相对时间:`dt_in = t_last − t_prev`、`dt_out = t_target − t_last`、`r = dt_out/dt_in`。不读视图路径 / 名称 / 池大小 / 绝对时间,因此伪装视图(时间整体平移)预测不变。表达按视图契约已是 log1p(CP10k),不再重算。+- HVG:两阶段 `covered_mask` 的交集内,按两阶段逐基因方差的均值取前 2,500 个(方差由稀疏矩阵 `E[x²]−E[x]²` 现场算,不稠密化全矩阵)。+- 联合 PCA:两阶段各随机取 ≤4,000 细胞,减共同均值后 `np.linalg.svd`,取前 30 个载荷 W;z 再除以全局标量 `s = sqrt(mean(z²))`。+- 训练(OT-CFM,arXiv:2302.00482 / arXiv:2210.02747):batch 256,batch 内用 `scipy.optimize.linear_sum_assignment` 对平方欧氏代价做**精确 OT 配对**(等价于 torchcfm 的 `ExactOptimalTransportConditionalFlowMatcher` 的耦合;本机 torchcfm 版本 API 不同,故直接用 scipy 实现同一配对,确定性);`x_t=(1−t)x_0+t x_1+σε`(σ=0.05),目标速度 `u_t=x_1−x_0`;v 为 MLP 31→256→256→256→30,SiLU,Adam lr 1e-3,4,000 步,时间上限 240 s,纯 CPU(`EXECUTION.json` `{"gpu": false}`)。+- 前推:t 固定为 1(只用训练区间端点的局部速度,不向 t>1 外推)。τ=α·r,α 默认 0.5,Euler K=8 步;每步位移范数上限 = 训练配对位移范数的 90% 分位(scaled 单位),总位移同样受此上限约束,防过冲。+- 解码:`Δx = (Δz·s)·Wᵀ` 只作用于 2,500 个 HVG,加到**该细胞自己**的表达上(残差与型内共变保留)。非零元:`max(0, x+Δx)`;零元只在 `Δx > θ` 时开启(θ = 全部正 Δx 的 95% 分位)。非 HVG 基因原样不动。+- 输出细胞:从最新阶段均匀抽样,`n_out = view_io.target_n_cells(manifest, n_last)`,组成不变。+- 单输入退路:只有一个输入阶段时无法学速度场,输出 = 最新阶段均匀抽样照抄(生长重采样关闭)。该分支只由 `len(inputs)` 决定。+- `--ablate`:`mechanism`/`velocity0`/`velocity`/`zero`/`copy_last` → Δz=0(同流程 copy_last);`random` → 保留每细胞位移范数、方向在 PCA 内均匀随机;`per_type_mean` → 每细胞速度换成本型 v 的均值(步长、上限、解码、θ 全同)。++## 知识来源++- arXiv:2302.00482(Tong 等,OT-CFM:minibatch 精确 OT 配对 + 条件流匹配);arXiv:2210.02747(Lipman 等,Flow Matching:直线路径与常速目标)。二者均为方法学文献,不含任何保留阶段测量值。+- 未使用 T1 发育事件表中的任何一行(本 draft 不生成新状态、不命名类型、不用标记程序),也未使用 `external/`、`prior/`。未使用 `uns.celltype_palette`。程序不含任何硬编码的阶段统计量、类型名单、比例或细胞数。++## 验证过什么++- `vec-check` 通过:X3 视图(两输入,E8.75+E9.0 → E9.5)与 proxy10 视图(单输入 E8.5 → E9.5)。+- X3 视图整流程约 90 s(4,000 步),proxy10 单输入分支约 2 s,内存远低于 28 GB。+- 训练损失从 0.260 降到 0.171(500 步冒烟 → 4,000 步),说明速度场确实拟合了 OT 配对方向;`cap_norm`(训练配对位移 90% 分位,scaled)≈3.46,α=0.5 时平均位移 |Δz·s| ≈8.4(log 表达单位,HVG 上),上限基本不触发过冲。+- 查分(A 半):+ - `proxy10`,单输入照抄分支:**52.97**(de_score 12.87 / de_direction 14.67 / mmd_u 15.50 / variogram 9.93;raw mmd_u 0.0406,raw variogram 0.0012)。与节点 5 的 proxy10 52.94 同一水平 —— 单输入分支就是照抄,符合预期,不含新信息。+ - `X3`,α=0.5、θ=95% 分位开启零元:**50.14**(de_score raw −0.1857 → 11.14 / de_direction raw −0.0173 → 12.34 / mmd_u raw 0.02667 → 17.66(skill 0.589,高于地板)/ variogram raw 0.001834 → 9.01(低于地板 10))。+- 结论(诚实):前向 OT-CFM 场在 X3 上**确实改善了细胞状态分布**(mmd_u 0.02667,skill 0.589 > 0.5,比地板好),方向上 DE 两项在地板附近,`variogram` 明显被损害(0.001834 vs 地板对应值)。总分 50.14 低于当前最佳节点 5 的 X3 55.32,**没有超过本 run 的最好节点**。损害 variogram 的嫌疑是解码时对 2,500 个 HVG 做稠密加法并开启零元(nnz 1,282,705 → 1,292,940),破坏了基因间共变结构。++## 没验证什么 / 待后续节点++- 因时间预算(本节点会话剩余额度用尽),只查到 α=0.5 主配置和 proxy10 的分;`velocity=0` 同流程对照、`random`、`per_type_mean` 三个对照,以及 α∈{0.25,1.0}、θ∈{90,99, 不开启零元}、多 seed 复跑都**未完成查分**,所以「机制相对同流程 copy_last 的净收益」尚未由查分证明,只能记为 unclear / 待测。`--ablate` 接口本身已实现(三个名字均可运行并改变输出)。+- 最有价值的下一步:(1) 关掉零元开启(θ→∞)并减小 HVG 数 / 只解码位移最大的少数基因,看 variogram 是否回到地板以上 —— 这是本方案当前唯一明确为负的组分;(2) 把 α 与 r 的关系改成向 0 收缩(α≈0.25)并跑 v=0 对照,确认 mmd_u 的收益是真的;(3) 与节点 5 的后向祖先位移机制**不同源**,若 (1)(2) 后 mmd_u 收益保住而 variogram 不再受损,可与节点 5 的 addnz 解码 / 型内选择组合。diff --git a/solution/run.py b/solution/run.pynew file mode 100644index 0000000..9e77cf1--- /dev/null+++ b/solution/run.py@@ -0,0 +1,249 @@+"""OT-CFM minimal draft (family ot_cfm).++Two input stages: learn a state-dependent velocity field v(z, t=1) in a shared+30-dim PCA space with minibatch exact-OT-paired conditional flow matching, then+integrate the output cells of the latest stage forward with a damped step+tau = alpha * r (r = dt_out / dt_in), decode the PCA displacement back onto the+HVG block and add it to each cell's own expression (residuals preserved).++One input stage: no velocity can be trained -> uniform-sample copy of the latest+stage (same fallback as the local_ot seeds).++Only relative time differences are used, so the prediction is invariant to a+uniform shift of all stage times.+"""++from __future__ import annotations++import argparse+import time++import numpy as np+from scipy import sparse+from scipy.optimize import linear_sum_assignment++from src.task1_temporal import view_io+++def log(*a):+ print(*a, flush=True)+++def parse_args():+ p = argparse.ArgumentParser()+ p.add_argument("--data", required=True)+ p.add_argument("--out", required=True)+ p.add_argument("--seed", type=int, default=0)+ p.add_argument("--ablate", default=None)+ p.add_argument("--alpha", type=float, default=0.5)+ p.add_argument("--theta-q", type=float, default=0.95, dest="theta_q")+ p.add_argument("--n-hvg", type=int, default=2500)+ p.add_argument("--n-pc", type=int, default=30)+ p.add_argument("--n-train", type=int, default=4000)+ p.add_argument("--steps", type=int, default=4000)+ p.add_argument("--batch", type=int, default=256)+ p.add_argument("--sigma", type=float, default=0.05)+ p.add_argument("--lr", type=float, default=1e-3)+ p.add_argument("--euler-k", type=int, default=8)+ p.add_argument("--time-cap", type=float, default=240.0)+ return p.parse_args()+++def per_gene_var(X: sparse.csr_matrix) -> np.ndarray:+ n = X.shape[0]+ m1 = np.asarray(X.mean(axis=0)).ravel()+ m2 = np.asarray(X.multiply(X).mean(axis=0)).ravel()+ return np.maximum(m2 - m1 * m1, 0.0), m1+++def main():+ args = parse_args()+ t_start = time.time()+ view = args.data+ manifest = view_io.load_manifest(view)+ genes = view_io.panel_genes(view, manifest)+ n_genes = len(genes)+ inputs = view_io.inputs_by_time(manifest)+ last = inputs[-1]+ rng = np.random.default_rng(args.seed)++ a_last = view_io.read_stage(view, last, genes, missing="fill")+ labels_last = np.asarray([str(v) for v in view_io.labels_of(a_last)])+ n_last = a_last.n_obs+ X_last = a_last.X.tocsr()++ n_out = view_io.target_n_cells(manifest, n_last)+ rows_out = view_io.sample_rows(n_last, n_out, rng)+ log(f"inputs={[e['stage'] for e in inputs]} n_last={n_last} n_out={n_out}")++ coords = None+ if manifest.get("needs_coords"):+ coords = rng.uniform(-1.0, 1.0, size=(n_out, 3)).astype(np.float32)++ dX = None # dense displacement over HVG columns, or None (copy_last)+ hvg_idx = None++ if len(inputs) >= 2:+ prev = inputs[-2]+ dt_in = float(last["time"]) - float(prev["time"])+ dt_out = float(manifest["target"]["time"]) - float(last["time"])+ r = (dt_out / dt_in) if dt_in > 0 else 0.0+ a_prev = view_io.read_stage(view, prev, genes, missing="fill")+ cov = view_io.covered_mask(view, last, genes) & view_io.covered_mask(view, prev, genes)+ X_prev = a_prev.X.tocsr()++ v_last, m_last = per_gene_var(X_last)+ v_prev, m_prev = per_gene_var(X_prev)+ cand = np.flatnonzero(cov)+ score = 0.5 * (v_last[cand] + v_prev[cand])+ keep = cand[np.argsort(-score)[: args.n_hvg]]+ keep = np.sort(keep)+ hvg_idx = keep+ log(f"r={r:.3f} hvg={len(keep)}")++ sub_last = view_io.sample_rows(n_last, min(args.n_train, n_last), rng)+ sub_prev = view_io.sample_rows(a_prev.n_obs, min(args.n_train, a_prev.n_obs), rng)+ A1 = np.asarray(X_last[sub_last][:, keep].todense(), dtype=np.float32)+ A0 = np.asarray(X_prev[sub_prev][:, keep].todense(), dtype=np.float32)+ mu = 0.5 * (A0.mean(0) + A1.mean(0))+ A0 -= mu+ A1 -= mu+ # joint PCA on the two-stage union+ both = np.concatenate([A0, A1], axis=0)+ U, S, Vt = np.linalg.svd(both, full_matrices=False)+ k = min(args.n_pc, Vt.shape[0])+ W = np.ascontiguousarray(Vt[:k].T.astype(np.float32)) # (hvg, k)+ z0 = A0 @ W+ z1 = A1 @ W+ s = float(np.sqrt((z0 ** 2).mean() + (z1 ** 2).mean())) + 1e-8+ del both, A0, A1, U, S, Vt+ z0s = (z0 / s).astype(np.float32)+ z1s = (z1 / s).astype(np.float32)+ log(f"pca k={k} scale={s:.4f} t={time.time()-t_start:.0f}s")++ import torch++ torch.manual_seed(args.seed)+ torch.use_deterministic_algorithms(False)+ dev = torch.device("cpu")+ t0 = torch.from_numpy(z0s).to(dev)+ t1 = torch.from_numpy(z1s).to(dev)+ dim = z0s.shape[1]++ net = torch.nn.Sequential(+ torch.nn.Linear(dim + 1, 256), torch.nn.SiLU(),+ torch.nn.Linear(256, 256), torch.nn.SiLU(),+ torch.nn.Linear(256, 256), torch.nn.SiLU(),+ torch.nn.Linear(256, dim),+ ).to(dev)+ opt = torch.optim.Adam(net.parameters(), lr=args.lr)+ g = torch.Generator(device="cpu").manual_seed(args.seed)+ pair_norms = []+ losses = []+ b = min(args.batch, t0.shape[0], t1.shape[0])+ deadline = t_start + args.time_cap+ for step in range(args.steps):+ if time.time() > deadline:+ log(f"time cap at step {step}")+ break+ i0 = torch.from_numpy(rng.integers(0, t0.shape[0], size=b)).to(dev)+ i1 = torch.from_numpy(rng.integers(0, t1.shape[0], size=b)).to(dev)+ x0 = t0[i0]+ x1 = t1[i1]+ with torch.no_grad():+ cost = torch.cdist(x0, x1).pow(2).cpu().numpy().astype(np.float64)+ _, perm = linear_sum_assignment(cost)+ x1 = x1[torch.from_numpy(perm).to(dev)]+ tt = torch.rand(b, generator=g, device=dev).unsqueeze(1)+ eps = torch.randn(b, dim, generator=g, device=dev)+ xt = (1.0 - tt) * x0 + tt * x1 + args.sigma * eps+ ut = x1 - x0+ vt = net(torch.cat([xt, tt], dim=1))+ loss = ((vt - ut) ** 2).mean()+ opt.zero_grad(set_to_none=True)+ loss.backward()+ opt.step()+ losses.append(float(loss))+ if step % 500 == 0:+ pair_norms.append(np.linalg.norm(ut.detach().numpy(), axis=1))+ log(f"step {step} loss {loss.item():.4f} t={time.time()-t_start:.0f}s")+ cap_norm = float(np.quantile(np.concatenate(pair_norms), 0.90)) if pair_norms else 0.0+ log(f"train done loss {losses[-1]:.4f} cap_norm={cap_norm:.3f} t={time.time()-t_start:.0f}s")++ # ---- integrate the output cells forward from the latest stage ----+ Xo = np.asarray(X_last[rows_out][:, keep].todense(), dtype=np.float32) - mu+ zo = torch.from_numpy((Xo @ W).astype(np.float32) / s).to(dev)+ tau = args.alpha * r+ net.eval()+ ab = (args.ablate or "").lower()++ if ab in ("mechanism", "velocity0", "velocity_0", "zero", "copy_last", "velocity") or tau <= 0:+ dz = torch.zeros_like(zo)+ elif ab == "per_type_mean":+ lab = labels_last[rows_out]+ dz = torch.zeros_like(zo)+ with torch.no_grad():+ v = net(torch.cat([zo, torch.ones(zo.shape[0], 1, device=dev)], 1))+ for name in np.unique(lab):+ sel = torch.from_numpy(np.flatnonzero(lab == name)).to(dev)+ dz[sel] = v[sel].mean(0, keepdim=True).expand(sel.shape[0], -1)+ dz = dz * tau+ nn_ = dz.norm(dim=1, keepdim=True)+ dz = torch.where(nn_ > cap_norm, dz * (cap_norm / nn_.clamp(min=1e-9)), dz)+ else:+ with torch.no_grad():+ if ab == "random":+ d = torch.randn(zo.shape, generator=g, device=dev)+ d = d / d.norm(dim=1, keepdim=True).clamp(min=1e-9)+ ref = net(torch.cat([zo, torch.ones(zo.shape[0], 1, device=dev)], 1))+ step_len = tau * ref.norm(dim=1, keepdim=True) / max(args.euler_k, 1)+ step_len = step_len.clamp(max=cap_norm / max(args.euler_k, 1))+ dz = d * (step_len * args.euler_k)+ else:+ z = zo.clone()+ h = tau / max(args.euler_k, 1)+ dz = torch.zeros_like(z)+ ones = torch.ones(z.shape[0], 1, device=dev)+ for _ in range(args.euler_k):+ vv = net(torch.cat([z, ones], 1))+ st = vv * h+ nrm = st.norm(dim=1, keepdim=True)+ st = torch.where(nrm > cap_norm, st * (cap_norm / nrm.clamp(min=1e-9)), st)+ dz = dz + st+ z = z + st+ nn_ = dz.norm(dim=1, keepdim=True)+ dz = torch.where(nn_ > cap_norm, dz * (cap_norm / nn_.clamp(min=1e-9)), dz)+ dz_np = dz.detach().numpy().astype(np.float32) * s+ dX = (dz_np @ W.T) # (n_out, n_hvg)+ log(f"displacement mean |dz|={np.linalg.norm(dz_np, axis=1).mean():.4f} t={time.time()-t_start:.0f}s")+ else:+ log("single input stage: uniform-sample copy of the latest stage")++ X_out = X_last[rows_out]+ if dX is None:+ Xf = X_out.copy()+ else:+ pos = dX[dX > 0]+ theta = float(np.quantile(pos, args.theta_q)) if pos.size else np.inf+ block = np.asarray(X_out[:, hvg_idx].todense(), dtype=np.float32)+ nz = block != 0+ new = np.where(nz, np.maximum(block + dX, 0.0), np.where(dX > theta, dX, 0.0))+ new = np.nan_to_num(new, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32)+ bm = sparse.csr_matrix(new)+ coo = bm.tocoo()+ hvg_cols = hvg_idx[coo.col]+ other_idx = np.setdiff1d(np.arange(n_genes), hvg_idx)+ Xo = X_out.tocsc()[:, other_idx].tocoo()+ rows_all = np.concatenate([coo.row, Xo.row])+ cols_all = np.concatenate([hvg_cols, other_idx[Xo.col]])+ data_all = np.concatenate([coo.data, Xo.data]).astype(np.float32)+ Xf = sparse.csr_matrix((data_all, (rows_all, cols_all)), shape=(n_out, n_genes))+ Xf.eliminate_zeros()+ log(f"theta={theta:.4f} nnz {X_out.nnz} -> {Xf.nnz}")++ view_io.write_prediction(Xf, genes, args.out, coords=coords, seed=args.seed)+ log(f"wrote {args.out} t={time.time()-t_start:.0f}s")+++if __name__ == "__main__":+ main()
调研来源?调研员查到并用到的知识条目和文献检索结果(只列标题和编号)。
用到的知识库条目
| 编号 | 标题 | 出处 |
|---|---|---|
| k034 | Flow matching and Schrödinger bridges offline: torchcfm (OT-CFM, SF2M), metric FM, DSB | arXiv:2302.00482 (OT-CFM, Tong et al.); arXiv:2307.03672 ([SF]2M); arXiv:2405.14780 (metric flow matching); arXiv:2106.01357 (DSB) |
| k031 | Offline OT toolkit in the sandbox: moscot TemporalProblem, wot OTModel, POT, geomloss | 10.1038/s41586-024-08453-2 (moscot); 10.1016/j.cell.2019.01.006 (Waddington-OT) |
| k009 | Conditional / OT flow matching for population transport | arXiv:2210.02747 (Lipman et al., Flow Matching for Generative Modeling) |
计划里引用的来源
- arXiv:2302.00482 — Tong 等 OT-CFM:minibatch 精确 OT 配对 + 条件流匹配损失,torchcfm 的 ExactOptimalTransportConditionalFlowMatcher(经知识条目 k034)
- arXiv:2210.02747 — Lipman 等 Flow Matching:直线条件路径 x_t=(1−t)x0+t·x1、目标速度 x1−x0;只有两个时间点时场识别较弱,需要对外推步长做阻尼(经知识条目 k009)
分析结果?分析员写的 ANALYSIS.json:改了什么、各组分数怎么变、假设是否成立、经验和下一步建议。
| 改了什么 | 新建 solution/run.py,实现 OT-CFM 最小版。两输入视图的流程是:取 2,500 个 HVG,做联合 30 维 PCA,batch 内用 scipy linear_sum_assignment 做精确 OT 配对,训练 MLP 速度场(4,000 步,时间上限 240 s,纯 CPU),在 t=1 处用 8 步 Euler 前推,步长受限,然后把位移解码回 HVG,加到细胞自身表达上,零元按 θ=95% 分位开启。单输入视图(proxy10)退化为从最新阶段均匀抽样照抄。另外提供 --ablate 开关(velocity0 / random / per_type_mean)。 |
|---|---|
| 各组分数的变化 | cell_state:proxy10 变坏:mmd_u 0.02981→0.0405,得分 19.55→15.53(−4.02),已降到地板附近(地板 15.00)。原因是 proxy10 走照抄分支,失去了 node 3 的组成调整,与 OT-CFM 机制无关 covariation:proxy10 变坏:variogram 0.001028→0.001233,得分 11.19→9.92(−1.27),略低于地板 10.00。同样只是照抄分支的结果,与机制无关 de_recovery:proxy10 变坏:de_score 0.2679→0.01639,得分 15.11→12.63(−2.48),落到地板 12.50 附近。属于照抄分支的结果 direction:proxy10 变坏:de_direction 0.36→0.1785,得分 17.82→14.67(−3.15)。属于照抄分支的结果。proxy10 照抄合计 52.75,与 Engineer 自报的 52.97、节点 5 的 52.94 处于同一水平,差异在噪声内 榜分:无(X3 崩溃,没有榜分)。表中只有 proxy10 的分解,四项得分合计 52.75;而对照 node 3 的四项合计 63.67,与它表中的总分 53.69 对不上,说明对照的榜分是多把尺子的汇总,两边不可直接相减 |
| 失败原因 | X3(两输入)在隔离重跑中崩溃,报 IndexError: losses[-1] list index out of range。直接原因是训练截止时间 deadline = t_start + time_cap(240 s),从程序启动就开始计时,而不是从训练开始计时。隔离环境是 aarch64 纯 CPU,stderr 里还有 cpuinfo 报错,比 Engineer 本地慢得多:读数据、算 HVG、做 SVD 已经超过 240 s(总耗时 275.4 s),训练循环在第 0 步就因 time cap 退出。于是 losses 和 pair_norms 都为空,取 losses[-1] 越界。即使不崩溃,cap_norm 也会是 0,模型也是未训练的。Engineer 在 METHOD.md 中自报的 X3 50.14(约 90 s 跑完)在本次重跑中没有复现,不能当作证据。 |
| family_id | ot_cfm |
| 假设是否成立 | unclear |
| 经验 |
|
| mechanism_active | unclear |
| 下一步建议 |
|
对话摘要?每个角色和大模型对话的统计:轮数、工具调用、用时、token 数和最后的回答摘录;原始记录只给路径。
只给统计和最后回答的摘录;完整对话请到原始记录位置里列出的文件看。
分析员
| 角色?调研员写计划、工程师改代码、分析员解读分数、审查员检查作弊。 | 分析员 vec-relay-claude-cli/claude-opus-5-5 |
|---|---|
| 调用次数 | 1 次 |
| 轮数?大模型一共回复了几轮(每轮可以调用多个工具)。 | 1 |
| 工具调用?大模型调用读文件、执行命令等工具的次数,按工具名统计。 | 共 0 次:— |
| 用时 | 不到 1 分 |
| token 数?输入 / 输出(含思考)的 token 数,来自接口返回的用量。 | 输入 2 · 输出 2,921 · 思考 0 |
| 任务(第一行) | 分析节点 n7 的打分结果,写 ANALYSIS.json |
| 最后的回答(摘录) | node 3 的四项合计 63.67,与它表中的总分 53.69 对不上,说明对照的榜分是多把尺子的汇总,两边不可直接相减", "cell_state": "proxy10 变坏:mmd_u 0.02981→0.0405,得分 19.55→15.53(−4.02),已降到地板附近(地板 15.00)。原因是 proxy10 走照抄分支,失去了 node 3 的组成调整,与 OT-CFM 机制无关", "covariation": "proxy10 变坏:variogram 0.001028→0.001233,得分 11.19→9.92(−1.27),略低于地板 10.00。同样只是照抄分支的结果,与机制无关", "de_recovery": "proxy10 变坏:de_score 0.2679→0.01639,得分 15.11→12.63(−2.48),落到地板 12.50 附近。属于照抄分支的结果", "direction": "proxy10 变坏:de_direction 0.36→0.1785,得分 17.82→14.67(−3.15)。属于照抄分支的结果。proxy10 照抄合计 52.75,与 Engineer 自报的 52.97、节点 5 的 52.94 处于同一水平,差异在噪声内" }, "hypothesis_supported": "unclear", "failure_reason": "X3(两输入)在隔离重跑中崩溃,报 IndexError: losses[-1] list index out of range。直接原因是训练截止时间 deadline = t_start + time_cap(240 s),从程序启动就开始计时,而不是从训练开始计时。隔离环境是 aarch64 纯 CPU,stderr 里还有 cpuinfo 报错,比 Engineer 本地慢得多:读数据、算 HVG、做 SVD 已经超过 240 s(总耗时 275.4 s),训练循环在第 0 步就因 time cap 退出。于是 losses 和 pair_norms 都为空,取 losses[-1] 越界。即使不崩溃,cap_norm 也会是 0,模型也是未训练的。Engineer 在 METHOD.md 中自报的 X3 50.14(约 90 s 跑完)在本次重跑中没有复现,不能当作证据。", "lessons": [ "给训练设时间上限时,计时起点应该是训练开始,并保证最少训练步数(如 ≥300 步)。不能从程序启动计时:评测环境(aarch64 纯 CPU)的前处理可能比本地慢 3 倍以上,会把训练预算整个吃掉。", "任何依赖循环结果的统计量(losses[-1]、quantile(pair_norms))都要对空列表做保护;循环一步没跑时应报错退出或回退到 v=0,不能静默使用未训练的网络和 cap_norm=0。", "Engineer 本地测得的耗时和分数不能代表隔离重跑:本节点本地约 90 s,重跑 275 s 并崩溃。时间敏感的参数要按 3 倍余量设置。", "单输入视图(proxy10)走照抄分支时,四项都会回到地板附近(合计约 52.75–52.97),相对组成系 node 3 会掉分。这个尺子上的变化不能用来评价两输入机制。" ], "next_suggestions": [ "[全部组,先修崩溃] 把 deadline 改成训练开始后计时,上限约 150 s,至少训练 500 步,并给 losses / pair_norms 加空列表保护。同时降低前处理和训练开销:n_train 2000、steps 2000、torch.set_num_threads 设为可用核数。然后在 X3 上重跑 α=0.5 主配置。", "[cell_state / 机制判定] 修好后,在 X3 上用同一 seed 跑 --ablate velocity0、random、per_type_mean 三个对照,并加跑 seed 1、2,逐项记录 mmd_u 原始值和得分。只有完整场同时优于 velocity0 和 random 超过 2 分,才算机制生效。", "[covariation] 把零元开启关掉(θ_q=1.0,即 θ=∞),只解码 |Δx| 最大的约 300–500 个 HVG,同时试 α=0.25,看 variogram 能否回到地板 10 分以上。Engineer 自报 variogram 0.001834 / 9.01,尚未验证。" ], "family_id": "ot_cfm", "mechanism_active": "unclear" } |
| 原始记录位置?完整对话记录在 Spark 上的文件路径;看板不复制原始内容。 | /home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/analyst.jsonl 5 KB |
工程师
| 角色 | 工程师 alibaba-token-plan-cn/qwen3.8-max |
|---|---|
| 调用次数 | 1 次(失败 1 次) |
| 轮数 | 24 |
| 工具调用 | 共 28 次:bash 25、write 2、edit 1 |
| 用时 | 18 分 |
| token 数 | 输入 54,710 · 输出 9,763 · 思考 6,875 |
| 任务(第一行) | 按计划修改程序:OT-CFM 最小版:PCA 中 minibatch-OT 配对学状态相关速度场,从最新阶段受限步长前推,加回细胞残差 |
| 报错 | exit 143: |
| 最后的回答(摘录) | — |
| 原始记录位置 | /home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/engineer.jsonl 169 KB /home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/engineer.stderr |
调研员
| 角色 | 调研员 vec-relay-claude-cli/claude-opus-5-5 |
|---|---|
| 调用次数 | 1 次 |
| 轮数 | 1 |
| 工具调用 | 共 0 次:— |
| 用时 | 1 分 |
| token 数 | 输入 2 · 输出 5,398 · 思考 0 |
| 任务(第一行) | 为节点 n7(草稿,父节点 nNone)写改进计划 PLAN.json,思路 ot_cfm |
| 最后的回答(摘录) | 始值。总分提升小于 2 分时,以多 seed 的配对差为准。", "expected_groups": ["cell_state", "direction", "de_recovery"], "risks": "(1) 两个边际不能唯一确定动力学:场可能主要学到扩散,或者学到从 E8.5 分布到 E9.5 分布的“平均化”方向。早期信号:输出在 PCA 上的总方差相对输入明显变大,或者 mmd_u 变好而 DE 两项相对 v=0 不变或变差。(2) 过冲:α=1 时 mmd_u 和 variogram 恶化,零值比例明显下降。监控输出零值比例和每基因方差相对输入的变化。(3) 速度退化为每型常向量:用 mechanism_evidence 中的型内 R² 检查;若 R²>0.9,且与“每型均值速度”对照分差在噪声内,就如实写成该机制没有贡献状态依赖性。(4) 与节点 5 机制相似(都用 OT 配对),收益可能重合。若不超过节点 5,如实报告;这只是 draft,不靠组合冲分。(5) 训练不稳或超时:先跑冒烟测试,设训练时间上限,损失曲线要下降并趋于平稳。(6) 零元开启阈值太低会变成稠密加法,损害 variogram。对比 θ 档位时同时看 variogram 原始值。", "family_id": "ot_cfm", "mechanism": "在共享 30 维 PCA 中,用 minibatch 精确 OT 配对的条件流匹配,学一个随细胞状态变化的速度场 v(x, t=1)。从最新阶段的每个真实细胞出发,按步长 α·r 受限积分,把 PCA 位移解码回 HVG,加到该细胞自己的表达上,残差得以保留。", "vs_constant_shift": "每型常数位移给同一类型的所有细胞同一个向量;全局组成重加权只改各型比例,不改表达。本方法的位移是 v 在每个细胞自身坐标上的取值,经多步积分后随状态弯曲,同一类型内不同细胞有不同的方向和幅度;组成完全不变(均匀抽样)。它与节点 5 也不同:节点 5 用后向的祖先重心位移,并且整体放大 3 倍;本方法用前向的平滑场,带步长上限,并在训练端点 t=1 做局部延续。", "mechanism_evidence": "报告以下几项。(a) 速度的型内离散程度:按源细胞标签分组,计算 v 的“型均值解释方差比” R² = 1 − Σ型内方差/总方差,以及每个细胞的 v 与本型均值速度的余弦相似度分布(中位数和 10% 分位)。(b) 改变了哪些细胞:位移范数在各型中的分布,变化最大的 20 个 HVG 及其符号(只作描述,不用于调参)。(c) 扩散检查:输出相对输入在 PCA 上的总方差比;输出与 v=0 输出之间的平均配对距离,以及它们各自到最新输入阶段的能量距离。(d) 四组分相对 v=0 的变化,按 score_parts 分开报告 mmd_u、variogram、de_score、de_direction 的原始值和 points。(e) 训练损失曲线;在一个留出的配对子集上,学到的 v 与配对方向 x1−x0 的余弦,应明显大于 0。", "mechanism_off_control": "用同一程序和同一 seed 加 --ablate 开关,跑三个对照。(1) velocity=0:输出应与同流程 copy_last 逐字节相同;若主配置的输出也与它相同,说明机制没有运行。(2) random:每个细胞的位移换成范数相同、方向在 PCA 中均匀随机的向量,用同一解码和同一零元阈值,检验收益是否只来自变宽或扩散;预期 mmd_u 和 variogram 不变或变差,DE 两项落回噪声。(3) per-type-mean:每个细胞的速度换成本型 v 的均值,即每型常数位移,步长相同,检验状态依赖性的贡献;预期 cell_state 和 covariation 不如完整场。只有完整场在 X3 上同时优于 (1) 和 (2),并且分差超过噪声(多 seed 确认),才算机制有效;若与 (3) 只差噪声内,就写成“方向有效但状态依赖性未体现”。", "sources": ["arXiv:2302.00482 — Tong 等 OT-CFM:minibatch 精确 OT 配对 + 条件流匹配损失,torchcfm 的 ExactOptimalTransportConditionalFlowMatcher(经知识条目 k034)", "arXiv:2210.02747 — Lipman 等 Flow Matching:直线条件路径 x_t=(1−t)x0+t·x1、目标速度 x1−x0;只有两个时间点时场识别较弱,需要对外推步长做阻尼(经知识条目 k009)"]} ``` |
| 原始记录位置 | /home/spark-longxinyang/vec/runs/test/20261003-153851-search-t1-D-smoke2h/nodes/7/researcher.jsonl 9 KB |