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&amp;E / mIF / RNA / text · 最终结果与独立复现 · 2026-10-30</div>
  <h1>四模态病理嵌入模型</h1>
  <p class="lede col">把 H&amp;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&amp;E 配对训练,H&amp;E 以外的模态两两之间的对齐是“涌现”出来的。</p>
<ul>
  <li><b>训练配对只用真实数据:</b>ORION 结直肠癌(H&amp;E–mIF)、HEST-1k Visium(H&amp;E–RNA)、TCGA/PathGen(H&amp;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&amp;E – mIF(16 个标记)</td><td class="mono">3,187 图块 · 约 40 名病人</td><td>训练 + 测试</td></tr>
    <tr><td>HEST-1k Visium</td><td>H&amp;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&amp;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&amp;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&amp;E 残差适配器,它同时读 UNI2 和 MedVTok 的特征。</p>
</div>
<div class="flow" role="img" aria-label="模型结构:四种模态各自经编码器和映射头进入 1536 维共享空间">
<div class="row he"><div class="b in">H&amp;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&amp;E 适配器和温度参数:<code>H&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;E↔mIF 上低 0.155。MedVTok 当锚点也更差(H&amp;E↔RNA 0.131 对 0.192)。</div></li>
  <li><div><b>多编码器后期融合是最大的单项提升。</b>RNA 用 4 个编码器、文本用 3 个,H&amp;E↔RNA +0.037、H&amp;E↔文本 +0.062,4 个涌现配对也全部提升(5/5 折)。拼接(早期融合)和同一编码器多种子平均都达不到这个效果,说明这些编码器是互补的。</div></li>
  <li><div><b>联合训练要只动 H&amp;E 一侧。</b>从头训练共享的 H&amp;E 头会让 H&amp;E↔mIF 变好,但涌现配对损失 26–74%。冻结所有模态头、只训练零初始化的残差适配器,H&amp;E↔mIF +0.10,涌现配对完全不变。</div></li>
  <li><div><b>MedVTok 作为适配器的额外输入有效,作为锚点无效。</b>额外输入使 H&amp;E↔mIF +0.015、H&amp;E↔文本 +0.016。换成同尺寸的 UNI2 随机投影就没有提升,说明增益来自 MedVTok 的信息而不是参数量。</div></li>
  <li><div><b>第二阶段的早停不稳定,3 个种子平均解决了这个问题。</b>验证曲线在 100 步后基本是平的,单次早停选中的步数在 100–900 之间波动,同一设置重跑 H&amp;E↔mIF 可以差 0.05。3 个早停适配器取平均后,H&amp;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&amp;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&amp;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&amp;E↔RNA 0.148 → 0.126;STPath 未见过的样本 0.086 → 0.080</td><td class="ok">可用</td></tr>
    <tr><td>GPT 合成描述</td><td>TCGA H&amp;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&amp;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&amp;E↔RNA 0.012)。训练集 HEST 里脑组织也只有 0.021。模型目前适用于训练数据覆盖的组织类型。</li>
  <li><b>H&amp;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&amp;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>