File size: 31,629 Bytes
e03b3d1 57065f1 e03b3d1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 | <!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>四模态病理嵌入模型</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@600;700&family=Noto+Sans+SC:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
/* Layout: one reading column (~68ch) with wide tables/figures allowed to break out; H&E stain palette (hematoxylin violet, eosin pink) */
:root {
--bg: #fbf9fb;
--surface: #f3eef4;
--fg: #231c2b;
--muted: #675d70;
--rule: #ddd3e0;
--hema: #4b3a8c; /* hematoxylin: accent, links, final model */
--eosin: #c2477a; /* eosin: second series */
--steel: #7f8aa3; /* neutral series */
--good: #2f7d5b;
--warn: #a8641a;
--bar-track: #ebe4ed;
--code-bg: #f0eaf2;
--f-display: "Noto Serif SC", "Songti SC", "STSong", serif;
--f-body: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
--f-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
--step-0: 15px; --step-1: 17px; --step-2: 21px; --step-3: 28px; --step-4: 36px;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #17131c; --surface: #211b28; --fg: #ece6f0; --muted: #a99fb3; --rule: #3a3143;
--hema: #a99af0; --eosin: #ec86b0; --steel: #9aa5bd; --good: #6cc59b; --warn: #e0a35a;
--bar-track: #2c2434; --code-bg: #241d2b; color-scheme: dark;
}
}
:root[data-theme="dark"] {
--bg: #17131c; --surface: #211b28; --fg: #ece6f0; --muted: #a99fb3; --rule: #3a3143;
--hema: #a99af0; --eosin: #ec86b0; --steel: #9aa5bd; --good: #6cc59b; --warn: #e0a35a;
--bar-track: #2c2434; --code-bg: #241d2b; color-scheme: dark;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--fg); font-family: var(--f-body); font-size: var(--step-0); line-height: 1.75; }
.wrap { max-width: 980px; margin: 0 auto; padding-inline: 20px; padding-block: 40px 80px; }
.col { max-width: 42em; }
header { display: grid; gap: 14px; padding-bottom: 28px; border-bottom: 1px solid var(--rule); }
.eyebrow { font-family: var(--f-mono); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
h1 { font-family: var(--f-display); font-size: var(--step-4); line-height: 1.25; margin: 0; text-wrap: balance; }
h2 { font-family: var(--f-display); font-size: var(--step-3); line-height: 1.3; margin: 56px 0 12px; text-wrap: balance; }
h3 { font-size: var(--step-1); margin: 28px 0 8px; }
p { margin: 0 0 12px; }
.lede { font-size: var(--step-1); color: var(--fg); }
.meta { font-size: 13px; color: var(--muted); }
a { color: var(--hema); }
code, .mono { font-family: var(--f-mono); font-size: .9em; }
code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px; }
pre.code { background: var(--code-bg); padding: 16px 18px; border-radius: 8px; overflow-x: auto; font-family: var(--f-mono); font-size: 13px; line-height: 1.6; margin: 12px 0 16px; }
ul, ol { padding-left: 1.3em; margin: 0 0 12px; }
li { margin: 4px 0; }
.tbl { overflow-x: auto; margin: 12px 0 18px; }
table { border-collapse: collapse; width: 100%; font-size: 14px; font-variant-numeric: tabular-nums; }
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--rule); vertical-align: top; }
th { font-weight: 500; color: var(--muted); font-size: 13px; white-space: nowrap; }
td.n, th.n { text-align: right; font-family: var(--f-mono); white-space: nowrap; }
tr.final td { background: var(--surface); font-weight: 500; }
tr.final td:first-child { box-shadow: inset 3px 0 0 var(--hema); }
.kind { font-family: var(--f-mono); font-size: 11px; letter-spacing: .04em; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--rule); color: var(--muted); white-space: nowrap; }
.kind.trained { color: var(--hema); border-color: var(--hema); }
.kind.emergent { color: var(--eosin); border-color: var(--eosin); }
.kind.virtual { color: var(--warn); border-color: var(--warn); }
.ok { color: var(--good); font-weight: 500; }
.bad { color: var(--warn); font-weight: 500; }
.note { color: var(--muted); font-size: 13px; }
.callout { background: var(--surface); border-radius: 8px; padding: 16px 20px; margin: 16px 0; }
.callout p:last-child { margin-bottom: 0; }
.panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); gap: 18px; margin: 16px 0 6px; }
.panel { min-width: 0; }
.panel h4 { margin: 0 0 6px; font-size: 14px; font-weight: 500; }
.panel svg { width: 100%; height: auto; display: block; }
.legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 13px; color: var(--muted); margin: 8px 0 0; }
.legend span::before { content: ""; display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: -1px; background: var(--c); }
.findings { display: grid; gap: 14px; counter-reset: f; padding: 0; list-style: none; }
.findings li { display: grid; grid-template-columns: 2.2em 1fr; gap: 6px; margin: 0; }
.findings li::before { counter-increment: f; content: counter(f); font-family: var(--f-mono); color: var(--hema); font-weight: 500; }
.findings b { font-weight: 700; }
.flow { background: var(--surface); border-radius: 8px; padding: 16px; margin: 12px 0 16px; display: grid; gap: 10px; overflow-x: auto; }
.flow .row { display: grid; grid-template-columns: 7.5em 1.2em minmax(11em, 1.4fr) 1.2em minmax(9em, 1fr) 1.2em 9em; align-items: center; min-width: 640px; }
.flow .b { border: 1px solid var(--rule); background: var(--bg); border-radius: 6px; padding: 6px 9px; font-size: 13px; line-height: 1.45; text-align: center; }
.flow .b small { display: block; color: var(--muted); font-size: 11.5px; }
.flow .in { font-weight: 500; }
.flow .he .b { border-color: var(--hema); }
.flow .z { border-color: var(--hema); color: var(--hema); font-weight: 700; }
.flow .ar { text-align: center; color: var(--muted); font-family: var(--f-mono); }
pre.mermaid { background: var(--surface); border-radius: 8px; padding: 16px; overflow-x: auto; margin: 12px 0 16px; text-align: center; }
.toc { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; margin-top: 4px; }
.toc a { text-decoration: none; color: var(--muted); }
.toc a:hover, .toc a:focus-visible { color: var(--hema); text-decoration: underline; }
a:focus-visible { outline: 2px solid var(--hema); outline-offset: 2px; }
@media (max-width: 560px) {
h1 { font-size: 28px; } h2 { font-size: 23px; }
th, td { padding: 6px 7px; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">ImageBind-style · H&E / mIF / RNA / text · 最终结果与独立复现 · 2026-10-30</div>
<h1>四模态病理嵌入模型</h1>
<p class="lede col">把 H&E 图像、多重免疫荧光(mIF,蛋白)、空间转录组(RNA)和病理文本放进同一个 1536 维空间。
锚点是冻结的 UNI2-h 图像空间;所有模态只用真实配对数据训练,虚拟和合成数据只用于评估。
最终模型(EJ-ad+MV,3 种子适配器平均)已训练完成,并提供从原始输入直接生成嵌入的接口 <code>embed.py</code>。
2026-10-29 又在独立 A800 环境中完成了发布模型评估、fold 0 重训、5 折重训和 release 重训,全部复现检查通过。</p>
<nav class="toc" aria-label="目录">
<a href="#setup">设定与数据</a><a href="#model">最终模型</a><a href="#results">结果</a><a href="#reproduction">独立复现</a><a href="#findings">主要发现</a>
<a href="#encoders">编码器选择</a><a href="#virtual">虚拟模态</a><a href="#limits">局限</a><a href="#usage">使用方法</a><a href="#files">文件</a>
</nav>
</header>
<section id="setup">
<h2>设定与数据</h2>
<div class="col">
<p>没有任何数据集同时测量四种模态,所以采用 ImageBind 的思路:每个模态只和 H&E 配对训练,H&E 以外的模态两两之间的对齐是“涌现”出来的。</p>
<ul>
<li><b>训练配对只用真实数据:</b>ORION 结直肠癌(H&E–mIF)、HEST-1k Visium(H&E–RNA)、TCGA/PathGen(H&E–文本)。</li>
<li><b>划分:</b>按病人(HEST 按研究|病人组)分成 train/val/test,5 折交叉验证;val 只用于早停,所有数字都是 test 上的。</li>
<li><b>虚拟和合成数据只用于测试:</b>STPath 虚拟 RNA、MIPHEI 虚拟 mIF、GPT 合成描述。它们只用来给“没有真实配对”的模态对构建测试集。</li>
<li><b>指标:</b>100 个样本的检索库里的 R@1(双向平均,随机水平 0.01),抽 50 次取平均;配对准确率是同一 100 库里的一对一匈牙利匹配(随机水平 0.01)。</li>
</ul>
</div>
<div class="tbl">
<table>
<thead><tr><th>数据集</th><th>配对</th><th>规模</th><th>用途</th></tr></thead>
<tbody>
<tr><td>ORION CRC</td><td>H&E – mIF(16 个标记)</td><td class="mono">3,187 图块 · 约 40 名病人</td><td>训练 + 测试</td></tr>
<tr><td>HEST-1k Visium</td><td>H&E – RNA(14,802 基因)</td><td class="mono">9,484 spots · 57 项研究 · 16 器官</td><td>训练 + 测试</td></tr>
<tr><td>TCGA / PathGen-1.6M</td><td>H&E – 文本</td><td class="mono">6,144 图像 · 6,115 条清洗后文本</td><td>训练 + 测试</td></tr>
<tr><td>STPath / MIPHEI / GPT 描述</td><td>虚拟 RNA、虚拟 mIF、合成文本</td><td class="mono">ORION、TCGA 测试图块</td><td>只用于测试</td></tr>
<tr><td>DLPFC(spatialLIBD)</td><td>H&E – RNA(脑)</td><td class="mono">482 spots · 3 名供体</td><td>外部测试</td></tr>
</tbody>
</table>
</div>
</section>
<section id="model">
<h2>最终模型</h2>
<div class="col">
<p>两阶段训练。第一阶段给每个编码器单独训练一个映射头,映射到冻结的 UNI2 空间。第二阶段冻结所有映射头,只训练一个 H&E 残差适配器,它同时读 UNI2 和 MedVTok 的特征。</p>
</div>
<div class="flow" role="img" aria-label="模型结构:四种模态各自经编码器和映射头进入 1536 维共享空间">
<div class="row he"><div class="b in">H&E 图像</div><div class="ar">→</div><div class="b">UNI2-h(冻结)<br>+ MedVTok(冻结)</div><div class="ar">→</div><div class="b">UNI2 + 残差适配器<small>3 个种子取平均</small></div><div class="ar">→</div><div class="b z">1536 维共享空间</div></div>
<div class="row "><div class="b in">mIF 图块</div><div class="ar">→</div><div class="b">VirTues<small>16 个标志物</small></div><div class="ar">→</div><div class="b">MLP 头</div><div class="ar">→</div><div class="b z">1536 维共享空间</div></div>
<div class="row "><div class="b in">RNA 计数</div><div class="ar">→</div><div class="b">hvg · Tahoe-x1<br>SE-600M · AIDO.Cell</div><div class="ar">→</div><div class="b">4 个线性头<small>后期融合</small></div><div class="ar">→</div><div class="b z">1536 维共享空间</div></div>
<div class="row "><div class="b in">文本</div><div class="ar">→</div><div class="b">KEEP · BiomedCLIP<br>Qwen3-Emb-0.6B</div><div class="ar">→</div><div class="b">3 个线性头<small>后期融合</small></div><div class="ar">→</div><div class="b z">1536 维共享空间</div></div>
</div>
<div class="col">
<ul>
<li><b>第一阶段:</b>每个映射头用 InfoNCE 对齐冻结的 UNI2(文本是多正样本 InfoNCE,同一图像的多条文本都算正样本)。RNA 和文本是后期融合:<code>z = normalise(Σ normalise(head_e(x_e)))</code>。</li>
<li><b>第二阶段:</b>只训练 H&E 适配器和温度参数:<code>H&E = UNI2 + mean_s A_s([UNI2, c·MedVTok])</code>。损失是三种配对损失之和加上对 UNI2 相似度结构的蒸馏(λ=1),学习率 1e-4,1000 步,按验证集三种 R@1 的几何平均早停。用 3 个随机种子各训练一个适配器,取平均。</li>
<li><b>后果:</b>mIF、RNA、文本的嵌入和第一阶段完全一样,所以涌现配对不受第二阶段影响;只有 H&E 这一侧变好。</li>
<li><b>发布版:</b><code>models/ej_ad_mv/</code>,用 fold 0 的 train + test 训练(ORION 3,000 图块、HEST 8,938 spots、TCGA 5,615 条文本),fold 0 的 val 用于早停。它没有自己的测试集,预期性能就是下面的 5 折交叉验证结果。</li>
</ul>
</div>
</section>
<section id="results">
<h2>结果</h2>
<h3>最终模型,5 折测试集</h3>
<div class="tbl">
<table>
<thead><tr><th>配对</th><th>类型</th><th>数据集</th><th class="n">R@1</th><th class="n">R@10</th><th class="n">配对准确率</th></tr></thead>
<tbody>
<tr><td>H&E ↔ mIF</td><td><span class="kind trained">训练</span></td><td>ORION</td><td class="n">0.801 ± 0.043</td><td class="n">0.991</td><td class="n">0.942</td></tr>
<tr><td>H&E ↔ RNA</td><td><span class="kind trained">训练</span></td><td>HEST</td><td class="n">0.203 ± 0.033</td><td class="n">0.625</td><td class="n">0.258</td></tr>
<tr><td>H&E ↔ 文本</td><td><span class="kind trained">训练</span></td><td>TCGA</td><td class="n">0.323 ± 0.007</td><td class="n">0.813</td><td class="n">0.347</td></tr>
<tr><td>mIF ↔ RNA(虚拟)</td><td><span class="kind emergent">涌现</span></td><td>ORION</td><td class="n">0.141 ± 0.007</td><td class="n">0.657</td><td class="n">0.194</td></tr>
<tr><td>mIF ↔ 文本(合成)</td><td><span class="kind emergent">涌现</span></td><td>ORION</td><td class="n">0.089 ± 0.008</td><td class="n">0.491</td><td class="n">0.104</td></tr>
<tr><td>文本 ↔ RNA(虚拟)</td><td><span class="kind emergent">涌现</span></td><td>TCGA</td><td class="n">0.095 ± 0.006</td><td class="n">0.436</td><td class="n">0.100</td></tr>
<tr><td>文本 ↔ mIF(MIPHEI 虚拟)</td><td><span class="kind emergent">涌现</span></td><td>TCGA</td><td class="n">0.046 ± 0.005</td><td class="n">0.271</td><td class="n">0.051</td></tr>
<tr><td>H&E ↔ RNA</td><td><span class="kind virtual">外部</span></td><td>DLPFC(脑)</td><td class="n">0.012</td><td class="n">0.112</td><td class="n">0.012</td></tr>
</tbody>
</table>
</div>
<p class="note col">随机水平:R@1 0.01,R@10 0.10,配对准确率 0.01。涌现配对的一侧是虚拟或合成数据,这些数据本身有误差,所以数值偏保守。DLPFC 是第一阶段(E)的结果;第二阶段不改变 RNA 侧。</p>
<h3>各阶段的提升(训练配对 R@1,5 折均值)</h3>
<div class="panels" id="chart"></div>
<div class="legend" aria-hidden="true">
<span style="--c: var(--steel)">单编码器(S4)</span>
<span style="--c: var(--eosin)">多编码器融合(E)</span>
<span style="--c: color-mix(in srgb, var(--hema) 55%, var(--bg))">+ H&E 适配器 / + MedVTok</span>
<span style="--c: var(--hema)">最终(3 种子平均)</span>
</div>
<h3>涌现配对(R@1)</h3>
<div class="tbl">
<table>
<thead><tr><th>阶段</th><th class="n">mIF↔RNA(虚拟)</th><th class="n">mIF↔文本(合成)</th><th class="n">文本↔RNA(虚拟)</th><th class="n">文本↔mIF(虚拟)</th></tr></thead>
<tbody>
<tr><td>单编码器,单独训练(S4)</td><td class="n">0.122</td><td class="n">0.076</td><td class="n">0.069</td><td class="n">0.038</td></tr>
<tr><td>单编码器,联合微调所有头(J8)</td><td class="n">0.122</td><td class="n">0.073</td><td class="n">0.065</td><td class="n">0.035</td></tr>
<tr><td>多编码器融合(E)</td><td class="n">0.141</td><td class="n">0.089</td><td class="n">0.095</td><td class="n">0.046</td></tr>
<tr><td>融合 + 联合微调 mIF 头(EJ-frz)</td><td class="n">0.134</td><td class="n">0.087</td><td class="n">0.095</td><td class="n">0.042</td></tr>
<tr class="final"><td>最终:只训练 H&E 适配器</td><td class="n">0.141</td><td class="n">0.089</td><td class="n">0.095</td><td class="n">0.046</td></tr>
</tbody>
</table>
</div>
</section>
<section id="reproduction">
<h2>独立复现:发布结果可重现</h2>
<div class="col">
<p><strong>总体结论:</strong>在 cuipeng 服务器的一张 NVIDIA A800-SXM4-80GB 上,从发布的编码器特征重新执行完整的 <code>reproduce.sh</code>。流程包括评估已发布 fold-0 checkpoint、重训 fold 0、重训 5 折、逐项比较发布结果,以及用 fold-0 train + test 重训 release 模型。任务最终状态为 <strong class="ok">Success</strong>,日志给出 <code>REPRODUCED within tolerance</code> 和 <code>FOURMODAL_REPRODUCTION_COMPLETE</code>。</p>
<ul>
<li><b>任务记录:</b><code>t-20261029185830-n649r</code>;实际运行约 9 分钟(2026-10-29 16:12–16:21:57 UTC)。</li>
<li><b>环境:</b>Python 3.12.1、PyTorch 2.7.1+cu128、CUDA 12.8、NumPy 2.4.6、Pandas 2.3.3、SciPy 1.16.3。</li>
<li><b>数据检查:</b>26 个特征数组,共 0.933 GiB;全部可读取、非空且无 NaN/Inf;三组数据的 5 折划分完整。</li>
<li><b>发布模型检查:</b>重新评估发布 fold-0 模型,7 个模态对与随模型发布的 <code>test.csv</code> 最大 R@1 差异为 0.0001。</li>
</ul>
</div>
<h3>fold 0 重训:留出测试集</h3>
<div class="tbl">
<table>
<thead><tr><th>模态对</th><th class="n">本次重训 R@1</th><th class="n">发布 R@1</th><th class="n">绝对差异</th><th>复现检查</th></tr></thead>
<tbody>
<tr><td>H&E ↔ mIF(ORION)</td><td class="n">0.7608</td><td class="n">0.7834</td><td class="n">0.0226</td><td class="ok">通过</td></tr>
<tr><td>H&E ↔ RNA(HEST)</td><td class="n">0.1853</td><td class="n">0.1917</td><td class="n">0.0064</td><td class="ok">通过</td></tr>
<tr><td>H&E ↔ 文本(TCGA)</td><td class="n">0.3240</td><td class="n">0.3203</td><td class="n">0.0037</td><td class="ok">通过</td></tr>
<tr><td>mIF ↔ RNA(虚拟,ORION)</td><td class="n">0.1358</td><td class="n">0.1352</td><td class="n">0.0006</td><td class="ok">通过</td></tr>
<tr><td>mIF ↔ 文本(合成,ORION)</td><td class="n">0.0978</td><td class="n">0.0987</td><td class="n">0.0009</td><td class="ok">通过</td></tr>
<tr><td>文本 ↔ RNA(虚拟,TCGA)</td><td class="n">0.0999</td><td class="n">0.1000</td><td class="n">0.0001</td><td class="ok">通过</td></tr>
<tr><td>文本 ↔ mIF(MIPHEI,TCGA)</td><td class="n">0.0484</td><td class="n">0.0491</td><td class="n">0.0007</td><td class="ok">通过</td></tr>
</tbody>
</table>
</div>
<h3>5 折重训:最终 <code>ens-es</code> 规则</h3>
<div class="tbl">
<table>
<thead><tr><th>模态对</th><th class="n">本次 5 折 R@1</th><th class="n">发布 R@1</th><th class="n">绝对差异</th><th>复现检查</th></tr></thead>
<tbody>
<tr><td>H&E ↔ mIF(ORION)</td><td class="n">0.7967</td><td class="n">0.8010</td><td class="n">0.0042</td><td class="ok">通过</td></tr>
<tr><td>H&E ↔ RNA(HEST)</td><td class="n">0.2021</td><td class="n">0.2030</td><td class="n">0.0009</td><td class="ok">通过</td></tr>
<tr><td>H&E ↔ 文本(TCGA)</td><td class="n">0.3231</td><td class="n">0.3232</td><td class="n">0.0001</td><td class="ok">通过</td></tr>
<tr><td>mIF ↔ RNA(虚拟,ORION)</td><td class="n">0.1414</td><td class="n">0.1414</td><td class="n">0.0000</td><td class="ok">通过</td></tr>
<tr><td>mIF ↔ 文本(合成,ORION)</td><td class="n">0.0885</td><td class="n">0.0887</td><td class="n">0.0001</td><td class="ok">通过</td></tr>
<tr><td>文本 ↔ RNA(虚拟,TCGA)</td><td class="n">0.0966</td><td class="n">0.0953</td><td class="n">0.0013</td><td class="ok">通过</td></tr>
<tr><td>文本 ↔ mIF(MIPHEI,TCGA)</td><td class="n">0.0464</td><td class="n">0.0465</td><td class="n">0.0001</td><td class="ok">通过</td></tr>
</tbody>
</table>
</div>
<p class="note col">所有 fold-0 和 5 折对比均落在发布代码预定义的容差内。最终还生成并独立加载验证了 <code>models/repro_fold0/model.pt</code>、<code>models/repro_release/model.pt</code> 和三张 <code>final_cv_*_repro.csv</code>。本次执行的是特征级复现;没有执行可选的 <code>RAW=1</code> 原始输入编码器检查。</p>
</section>
<section id="findings">
<h2>主要发现</h2>
<ol class="findings col">
<li><div><b>UNI2-h 是最好的冻结锚点。</b>在 mIF、RNA、文本三种训练配对上都没有被显著超过;GigaPath 接近(RNA 差 0.035),CONCH 在 H&E↔mIF 上低 0.155。MedVTok 当锚点也更差(H&E↔RNA 0.131 对 0.192)。</div></li>
<li><div><b>多编码器后期融合是最大的单项提升。</b>RNA 用 4 个编码器、文本用 3 个,H&E↔RNA +0.037、H&E↔文本 +0.062,4 个涌现配对也全部提升(5/5 折)。拼接(早期融合)和同一编码器多种子平均都达不到这个效果,说明这些编码器是互补的。</div></li>
<li><div><b>联合训练要只动 H&E 一侧。</b>从头训练共享的 H&E 头会让 H&E↔mIF 变好,但涌现配对损失 26–74%。冻结所有模态头、只训练零初始化的残差适配器,H&E↔mIF +0.10,涌现配对完全不变。</div></li>
<li><div><b>MedVTok 作为适配器的额外输入有效,作为锚点无效。</b>额外输入使 H&E↔mIF +0.015、H&E↔文本 +0.016。换成同尺寸的 UNI2 随机投影就没有提升,说明增益来自 MedVTok 的信息而不是参数量。</div></li>
<li><div><b>第二阶段的早停不稳定,3 个种子平均解决了这个问题。</b>验证曲线在 100 步后基本是平的,单次早停选中的步数在 100–900 之间波动,同一设置重跑 H&E↔mIF 可以差 0.05。3 个早停适配器取平均后,H&E↔文本比单种子再 +0.015(5/5 折,p=0.02),也是三种规则里几何平均最高的。</div></li>
<li><div><b>线性头比 MLP 头更适合 RNA 和文本。</b>训练配对持平,涌现配对 +5%;mIF 保留 MLP 头。</div></li>
<li><div><b>用虚拟配对直接训练不如走锚点的涌现路线。</b>文本↔蛋白在真实 mIF 上 0.596 对 0.678(AUROC)。所以虚拟数据只用作测试,从不进入训练。</div></li>
<li><div><b>实例级的涌现对齐仍然弱。</b>mIF↔RNA 0.141 是随机水平的 14 倍,但比训练配对低一个数量级;概念级对齐(AUROC 0.6–0.7)更可靠。</div></li>
</ol>
</section>
<section id="encoders">
<h2>编码器选择</h2>
<div class="tbl">
<table>
<thead><tr><th>模态</th><th>最终使用</th><th>依据(5 折配对检验)</th></tr></thead>
<tbody>
<tr><td>H&E</td><td>UNI2-h + MedVTok(只进适配器)</td><td>UNI2 在所有训练配对上不被显著超过;MedVTok 额外输入 mIF +0.015、文本 +0.016</td></tr>
<tr><td>mIF</td><td>VirTues(16 标记)+ MLP 头</td><td>从头训练的 ConvNeXt-T 在 H&E↔mIF 上高 0.03,但 VirTues 的跨模态迁移好得多(mIF↔markers 0.66 对 0.48),且不绑定固定标记面板</td></tr>
<tr><td>RNA</td><td>hvg + Tahoe-x1 + SE-600M + AIDO.Cell,各一个线性头</td><td>单个基础模型最多 +0.016;4 个融合 +0.037,接近 10 个融合(+0.044)</td></tr>
<tr><td>文本</td><td>KEEP + BiomedCLIP + Qwen3-Emb-0.6B,各一个线性头</td><td>KEEP 单个最好(0.278);3 个融合 0.299,接近 5 个融合(0.303)</td></tr>
</tbody>
</table>
</div>
</section>
<section id="virtual">
<h2>虚拟模态(只用于评估)</h2>
<div class="col">
<p>每种虚拟模态先做校准:在有真实数据的同一批样本上,用虚拟数据替换真实数据,看配对结果是否接近。</p>
</div>
<div class="tbl">
<table>
<thead><tr><th>虚拟模态</th><th>校准(真实 → 虚拟,同一批样本)</th><th>结论</th></tr></thead>
<tbody>
<tr><td>STPath 虚拟 RNA</td><td>HEST H&E↔RNA 0.148 → 0.126;STPath 未见过的样本 0.086 → 0.080</td><td class="ok">可用</td></tr>
<tr><td>GPT 合成描述</td><td>TCGA H&E↔文本 0.219 → 0.162(约 0.75 倍)</td><td class="ok">可用,偏保守</td></tr>
<tr><td>MIPHEI 虚拟 mIF</td><td>ORION 未见过的测试图块:配对准确率 0.92 → 0.55(约 0.6 倍)</td><td class="ok">可用</td></tr>
<tr><td>GigaTIME 虚拟 mIF</td><td>ORION H&E↔mIF 0.644 → 0.038</td><td class="bad">不可用,已弃用</td></tr>
</tbody>
</table>
</div>
</section>
<section id="limits">
<h2>局限</h2>
<ul class="col">
<li><b>外部泛化差:</b>脑组织 DLPFC 上所有非循环配对都在随机水平(H&E↔RNA 0.012)。训练集 HEST 里脑组织也只有 0.021。模型目前适用于训练数据覆盖的组织类型。</li>
<li><b>H&E↔文本的 MedVTok 增益可能偏高:</b>MedVTok 的预训练图像包含 PathGen 1.6M,见过我们的 TCGA 测试图像(只有图像,没有文本)。ORION 上的 mIF 增益不受影响。</li>
<li><b>验证集小:</b>ORION 每折只有 187–276 个验证图块,所以第二阶段用了 3 种子平均来降低早停的随机性。</li>
<li><b>涌现配对的测试数据一侧是虚拟或合成的,</b>数值是保守估计,不是真实数据上的精确值。</li>
<li><b>新数据的 RNA 批次效应:</b>默认用 HEST 训练集的统计量做标准化;新平台或新实验室的数据建议用 <code>zscore="self"</code>(外部测试就是这样做的)。</li>
</ul>
</section>
<section id="usage">
<h2>使用方法</h2>
<div class="col">
<p>运行环境:<code>.venv/bin/python -I</code>,需要 GPU。所有输出都是 L2 归一化的 (N, 1536) 向量,相似度就是点积。</p>
</div>
<pre class="code">from embed import Embedder
E = Embedder("models/ej_ad_mv") # 发布版模型
z_he = E.he(images) # H&E 图块:PIL 图像或 (N,H,W,3) uint8;约 0.5 µm/px,112–166 µm 视野
z_txt = E.text(["Tumor cells with ...", ...])
z_rna = E.rna(counts, genes, zscore="train") # 原始计数(spots × 基因)+ HGNC 基因名;新数据集可用 zscore="self"
z_mif = E.mif(tiles, slide) # (N,16,H,W) mIF 图块,通道顺序 = embed.MIF_MARKERS,8 位强度;附每个图块的切片 ID
sim = z_he @ z_rna.T # 跨模态相似度
# 已有特征时:E.he_from_feats(uni2, medvtok)、E.mif_from_feats(virtues)、E.rna_from_feats({...})、E.text_from_feats({...})</pre>
<h3>接口验证(<code>53_embed_check.py</code>)</h3>
<div class="col"><p>从原始像素、字符串和计数矩阵出发,用 <code>embed.py</code> 重新计算特征,和训练时缓存的特征逐项比较;再用 fold 0 模型在留出测试集上做端到端检索。</p></div>
<div class="tbl">
<table>
<thead><tr><th>检查</th><th>结果</th></tr></thead>
<tbody>
<tr><td>UNI2-h、MedVTok(ORION、PathGen、HEST 图像)</td><td class="mono">余弦中位数 1.0000,最低 ≥ 0.9997</td></tr>
<tr><td>KEEP、BiomedCLIP、Qwen3(TCGA 文本)</td><td class="mono">中位数 1.0000 / 1.0000 / 0.9998(BiomedCLIP 个别 0.93,bf16 批次差异)</td></tr>
<tr><td>hvg、AIDO.Cell / Tahoe-x1、SE-600M(HEST)</td><td class="mono">1.0000 / 0.991、0.994(后两者按官方做法随机采样基因)</td></tr>
<tr><td>VirTues(全部 3,187 个 ORION 图块)</td><td class="mono">中位数 1.0000,最低 1.0000</td></tr>
<tr><td>端到端 R@1:原始输入 vs 缓存特征</td><td class="mono">mIF 0.792 / 0.792 · RNA 0.194 / 0.194 · 文本 0.335 / 0.336</td></tr>
</tbody>
</table>
</div>
</section>
<section id="files">
<h2>文件</h2>
<div class="tbl">
<table>
<thead><tr><th>路径</th><th>内容</th></tr></thead>
<tbody>
<tr><td><code>embed.py</code></td><td>推理接口(Embedder、Bind4)</td></tr>
<tr><td><code>52_final_model.py</code></td><td>最终模型训练:<code>cv</code>(选择规则)、<code>fold0</code>、<code>release</code></td></tr>
<tr><td><code>53_embed_check.py</code></td><td>原始输入验证,结果在 <code>results/embed_check.csv</code></td></tr>
<tr><td><code>models/ej_ad_mv/</code></td><td>发布版模型:<code>model.pt</code>(77 MB,含标准化统计量)、<code>meta.json</code>、验证曲线</td></tr>
<tr><td><code>models/ej_ad_mv_fold0/</code></td><td>fold 0 模型及其留出测试结果 <code>test.csv</code></td></tr>
<tr><td><code>results/final_cv_*.csv</code></td><td>5 折 × 3 种子的选择规则实验</td></tr>
<tr><td><code>RESULTS.md</code></td><td>全部实验的完整记录(01_–53_)</td></tr>
</tbody>
</table>
</div>
</section>
</div>
<script>
(function () {
// Trained-pair R@1, 5-fold means (RESULTS.md: 48_ S4 / E, 51_ EJ-ad / EJ-ad+MV, 52_ cv ens-es)
var stages = ["单编码器 S4", "多编码器融合 E", "+ H&E 适配器", "+ MedVTok", "最终(3 种子)"];
var colors = ["var(--steel)", "var(--eosin)", "color-mix(in srgb, var(--hema) 55%, var(--bg))",
"color-mix(in srgb, var(--hema) 55%, var(--bg))", "var(--hema)"];
var pairs = [
{ name: "H&E ↔ mIF(ORION)", max: 1.0, ticks: [0, 0.25, 0.5, 0.75, 1.0], v: [0.675, 0.675, 0.776, 0.792, 0.801] },
{ name: "H&E ↔ RNA(HEST)", max: 0.25, ticks: [0, 0.05, 0.1, 0.15, 0.2, 0.25], v: [0.155, 0.192, 0.201, 0.200, 0.203] },
{ name: "H&E ↔ 文本(TCGA)", max: 0.35, ticks: [0, 0.1, 0.2, 0.3], v: [0.236, 0.299, 0.299, 0.315, 0.323] }
];
var NS = "http://www.w3.org/2000/svg";
var W = 320, labelW = 104, valW = 40, rowH = 26, top = 6, axisH = 22;
var plotW = W - labelW - valW;
var H = top + stages.length * rowH + axisH;
function el(n, a, t) { var e = document.createElementNS(NS, n); for (var k in a) e.setAttribute(k, a[k]); if (t != null) e.textContent = t; return e; }
var host = document.getElementById("chart");
pairs.forEach(function (p) {
var div = document.createElement("div"); div.className = "panel";
var h = document.createElement("h4"); h.textContent = p.name; div.appendChild(h);
var svg = el("svg", { viewBox: "0 0 " + W + " " + H, role: "img", "aria-label": p.name + " R@1: " + stages.map(function (s, i) { return s + " " + p.v[i].toFixed(3); }).join(",") });
var x = function (v) { return labelW + v / p.max * plotW; };
p.ticks.forEach(function (t) {
svg.appendChild(el("line", { x1: x(t), x2: x(t), y1: top, y2: top + stages.length * rowH, stroke: "var(--rule)", "stroke-width": 1 }));
svg.appendChild(el("text", { x: x(t), y: H - 6, "text-anchor": "middle", "font-size": 10, fill: "var(--muted)", "font-family": "var(--f-mono)" }, t === 0 ? "0" : String(t)));
});
stages.forEach(function (s, i) {
var y = top + i * rowH;
svg.appendChild(el("text", { x: labelW - 8, y: y + rowH / 2 + 4, "text-anchor": "end", "font-size": 11, fill: "var(--fg)" }, s));
svg.appendChild(el("rect", { x: labelW, y: y + 5, width: x(p.v[i]) - labelW, height: rowH - 10, rx: 2, fill: colors[i] }));
svg.appendChild(el("text", { x: x(p.v[i]) + 5, y: y + rowH / 2 + 4, "font-size": 11, fill: i === stages.length - 1 ? "var(--hema)" : "var(--muted)", "font-family": "var(--f-mono)", "font-weight": i === stages.length - 1 ? 600 : 400 }, p.v[i].toFixed(3)));
});
div.appendChild(svg); host.appendChild(div);
});
})();
</script>
</body>
</html>
|