跳到主要内容

肺癌项目(上):为什么不能一把梭,以及数据怎么变成样本

这一章讲三件事: 这本书后半段的大项目——从胸部 CT 找肺结节——是怎么设计的;CT 数据这种「野数据」有多少要先懂的规矩;以及原始文件怎么一步步变成第 07 章那种 __getitem__ 能吐出来的样本。 位置:Part 2 的转折点。从这里开始,「理解数据」比「设计模型」更占篇幅——这是作者们刻意让你体味的真实项目结构。

1. 任务与它的难度来源

任务一句话:输入一个病人胸部的 CT 扫描,输出疑似恶性肿瘤的位置。1

早期发现肺癌极大地提高存活率,但人工筛片是「在 100 个草垛里找针」:每一张片都要受过多年训练的专家盯着看,而绝大多数片子里什么都没有——人脑不擅长这种单调的警觉1

那为什么不让神经网络直接读整张 CT?书里给了一个数量级答案2:

即使是恶性肿瘤病人,一张 CT 里也有高达 99.9999% 的体素不是癌。 书里给了两个生活化类比:相当于高清电视屏幕上两个像素的色差;相当于一书架小说里一个拼错的词。

端到端(不给模型定中间步骤、输入直接到答案)的做法在通用视觉上很成功,但那是在几十万张图、类别不稀缺的条件下;这里两个条件都没有2

解法:三步流水线

① 数据加载(本章) 原始文件 → 能按候选点取出小方块数据
② 分割(第 15 章) 整张 CT → 标出「可能是结节」的体素(热图)
③ 分类(第 13–14 章) 每个候选点的小方块 → 结节/非结节

图说:像工厂的装配线,每站只干一件事。
注意顺序:书先做 ③ 再做 ②——因为标注数据直接支持 ③,而 ② 最难。

分步的代价书里写明了:两个模型不联合训练,梯度不能从最终答案一路回传到最前——上限会低一些,但每一步可教、可调、可换人接手2。临床上也更想要「标出可疑点供医生复核」,而不是一个黑盒二元诊断。

还有一句必须听进去的作者声明:这个项目跑出来的结果不能用于临床——它是教学载体,不是医疗设备3

2. 先懂数据:CT 的规矩

CT 扫描可以粗理解为「三维 X 光」:X 光是三维身体投影到二维平面,CT 保住了第三维——机器绕身体一圈圈扫,计算机反推出每个小体积的密度,得到一整块三维数组。这个三维数组里的每个单元叫体素(voxel,三维版像素)4

四条规矩,条条后面都要用4:

  1. 数值是 Hounsfield 单位(HU):空气 ≈ −1000,水 = 0,骨 ≥ +1000。肿瘤大约在水附近。
  2. 体素通常不是立方体:行列方向(横断面)间距小,头脚方向间距大(比如 1.125×1.125×2.5 毫米)——因为病人是躺在床上沿头脚方向移动的,这条轴的采样方式不同。书里有句方法论:这个事实是作者从机器照片里病人移动的方向推出来的——懂你的数据,连物理来源都要懂4
  3. 原生格式是 DICOM,1984 年的医学影像标准,复杂;好在 LUNA 数据集已经转成了好读得多的 MetaIO 格式(.mhd 头 + .raw 体)。
  4. 每张 CT 约 512×512×(100~250) 体素,约 3200 万个数——而最小的结节只有几毫米,约「比整张 CT 小一百万倍」4

3. 标注数据:脏才是常态

数据源是 LUNA16 大挑战:888 张 CT 扫描,带人工标注,外加一个公开排行榜(比方法成绩的公开榜单)5

两个 CSV 文件,第一次见识真实数据的脏6:

  • candidates.csv:551066 行,「看着像结节的候选点」——每行是 seriesuid(那张 CT 的唯一编号)+ 三维坐标 + class(1=真是结节,0=不是)。其中真是结节的只有 1351 个
  • annotations.csv:1187 行,给其中一部分结节标了直径(毫米)。

两个文件的坐标对不齐:同一个结节,一边写 (−128.70, −175.32, −298.39),另一边写 (−128.94, −175.04, −297.87)——都是「中心附近」,谁也不精确等于谁6。书里选择做模糊匹配:两点各轴差值都不超过「直径÷4」就算同一个(这是对「直径/2 是半径、容差再砍半」的朴素实现)。「真实数据集常常就是这样,你得自己做这种缝合」6

把两份数据缝成一张干净表之后,按结节直径从大到小排序——后面每隔 10 个取一个做验证集时,大小分布才有代表性,否则验证集可能全是极端尺寸,模型看着像不行6

4. 主走查:从一个坐标到一个样本

现在跟一条数据走全程:从 candidates.csv 的一行,到 Dataset 吐出的四元组7

输入:candidates.csv 一行
seriesuid=1.3.6…, coord=(-56.08, -67.85, -311.92), class=0

① 读 CT:SimpleITK 库读 .mhd(自动带上 .raw)→ 512×512×N 的 NumPy 数组
② 截断:HU 值 clip 到 [−1000, +1000] —— 视野外的负值标记、金属植入的极端值
都是会带歪批归一化统计量的离群点,切掉
③ 换坐标:标注给的是患者坐标系(毫米,原点在机器里任意处);
数组要的是体素下标 (I,R,C)。四步换算:
减原点 → 乘方向矩阵的逆 → 除体素尺寸 → 取整
(方向矩阵和体素尺寸都在 .mhd 头里)
④ 切块:以换算后的中心,切一个 32×48×48 的小方块
⑤ 组装样本四元组:
(张量 [1,32,48,48], 标签 [0,1] 或 [1,0], series_uid, 中心下标 (91,360,341))

图说:五步,每一步都在「驯化」数据;模型只见第 ⑤ 步的产物。

第 ③ 步那套坐标换算(毫米↔体素)是本章最容易翻车的细节,书里给了完整的四步和逆变换;我们的拆解只要记住一句:标注坐标和数组下标活在两个坐标系里,中间隔着原点、体素尺寸、方向矩阵三个元数据7

Dataset 与缓存

LunaDataset 实现第 07 章那两个方法:__len__ 返回候选数,__getitem__ 走上面五步返回四元组——至此,野数据和 PyTorch 训练循环接上了8

一个性能细节成了设计决策:每取一个样本都从头读整张 CT(每张上百 MB)是不可接受的——不加缓存,整个数据集慢 50 倍。书里上了两层缓存:整张 CT 用内存缓存(只留最近一张),切好的小方块用磁盘缓存(下次直接从盘里读 float32 小块)。所以 part 2 要额外约 100GB 缓存空间9

5. 作者的判断与证据

说法性质
99.9999%、小一百万倍数量级估算,为了说明「端到端不可行」——方向可靠,精确值别引用2
「结果不能用于临床」作者声明,写到注记里3
坐标对不齐、模糊匹配规则有据,书里打印了两组坐标;直径÷4 是作者定的工程规则,无理论依据6
「缓存让数据集快 50 倍」作者实测口径(「easily 50 times」),环境相关9
「真实项目里数据工作占大头」作者的经验论;第 13 章会给它一个更具体的注脚(到第 13 章才第一次设计模型)

6. 边界与局限

  • LUNA 数据在医学数据里算干净的;书里警告:野外的 CT 各扫描仪脾性不同(有的用负密度标记视野外),换数据源要重新核假设5
  • 分割只做 2D 切片而非 3D 整体,是拿精度换简单(第 15 章);切片厚度不一致的问题被「让模型学着扛」一句带过。
  • 患者级泄漏(同一病人的多张 CT 分到两边)本章提了原则没做处理——该数据集里同病人多扫描的情况少,换项目要重查8
  • 排行榜文化是把双刃剑:书里引它做灵感来源,没讨论「为榜单过拟合」的问题。

7. 可带走的

  1. 稀有目标 + 有限数据 ⟹ 别端到端;拆成「找候选 → 判候选」两步,每步的数据需求都可承受。
  2. 先懂数据再建模:HU 单位、非立方体素、坐标系——不懂就会在错误的方向上 debug。
  3. 真实标注是脏的:两个官方文件的中心坐标都对不齐;模糊匹配、排序抽样是常态手艺。
  4. 隔离脏数据:解析/缝合/清洗的代码和训练代码分开;样本格式一旦定下来,训练循环只认四元组。
  5. 离群值会带歪归一化统计——clip 掉业务上无关的极端值。
  6. 缓存是设计决策不是优化装饰:无缓存慢 50 倍;改了生成函数要清缓存。
  7. 切分要有代表性:按大小排序后等距取样,避免验证集全是极端样本。

8. 原文地图

主题原书章原文位置
任务与草垛找针ch11text/19-ch11-11-using-pytorch-to-fight-cancer.txt:48(搜「survival rate」) · :53(搜「100 haystacks」)
99.9999% 与类比ch11text/19-ch11-11-using-pytorch-to-fight-cancer.txt:418(搜「99.9999%」)
三步流水线与分段代价ch11text/19-ch11-11-using-pytorch-to-fight-cancer.txt:257(搜「use three main」) · :462(搜「won’t be trained in tandem」)
非临床声明ch11text/19-ch11-11-using-pytorch-to-fight-cancer.txt:86(搜「enough to use clinically」)
CT/体素/HU/非立方ch11text/19-ch11-11-using-pytorch-to-fight-cancer.txt:158(搜「3D X-rays」) · :163(搜「voxel」) · :244(搜「the patient actually moves along」)
LUNA 与下载ch11text/19-ch11-11-using-pytorch-to-fight-cancer.txt:593(搜「LUNA」) · :627(搜「60 GB」)
两个 CSV 与 1351ch12text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:191(搜「551,000 lines」) · :196(搜「1,351 candidates」)
坐标对不齐与模糊匹配ch12text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:246(搜「-128.70」) · :335(搜「annotationDiameter_mm / 4」)
HU clip 与离群点ch12text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:491(搜「clip(-1000, 1000」) · :497(搜「outlier」)
坐标换算四步ch12text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:666(搜「Flip the coordinates」)
切块与样本四元组ch12text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:744(搜「getRawCandidate」) · :894(搜「LunaDataset()[0]」)
Dataset 契约与切分ch12text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:821(搜「two functions」) · :981(搜「val_stride」)
缓存 50 倍ch12text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:920(搜「easily 50 times」)

Footnotes

  1. 出处:「11 Using PyTorch to fight cancer」第 48 段(text/19-ch11-11-using-pytorch-to-fight-cancer.txt:48,搜「survival rate」)与第 53 段(:53,搜「100 haystacks」)。早诊极大影响存活率;人工筛片「被放在 100 个草垛前,问哪些里有针」,漏报常发生在早期征兆微妙的阶段。 2

  2. 出处:「11 Using PyTorch to fight cancer」第 418 段(text/19-ch11-11-using-pytorch-to-fight-cancer.txt:418,搜「99.9999%」)、第 435 段(:435,搜「Fast R-CNN」)与第 462 段(:462,搜「won’t be trained in tandem」)。端到端模型如 Fast R-CNN 需要几十万张图且不受稀有类限制;分步方案「可能限制上限,但带来好得多的学习体验」。 2 3 4

  3. 出处:「11 Using PyTorch to fight cancer」第 86 段(text/19-ch11-11-using-pytorch-to-fight-cancer.txt:86,搜「enough to use clinically」)。原文注记:「part 2 的最终产物能跑,但精度不足以临床使用;我们用它教 PyTorch,不是用尽技巧真去解题。」 2

  4. 出处:「11 Using PyTorch to fight cancer」第 158 段(text/19-ch11-11-using-pytorch-to-fight-cancer.txt:158,搜「3D X-rays」)、第 163 段(:163,搜「voxel」)与第 244 段(:244,搜「the patient actually moves along」)。CT≈3D X 光、单通道三维数组;HU 标度(空气 −1000、水 0、骨 +1000)与患者沿头脚轴移动导致非立方体素,见 ch11 与 ch12 的 HU 小节(text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:482,搜「Hounsfield」)。 2 3 4

  5. 出处:「11 Using PyTorch to fight cancer」第 593 段(text/19-ch11-11-using-pytorch-to-fight-cancer.txt:593,搜「LUNA」)与第 609 段(:609,搜「incredibly messy」)。LUNA16 两赛道 NDET/FPRED,上榜需交论文;「野外的 CT 在各种扫描仪和处理程序间极其混乱」。 2

  6. 出处:「12 Combining data sources into a unified dataset」第 191 段(text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:191,搜「551,000 lines」)、第 196 段(:196,搜「1,351 candidates」)、第 246 段(:246,搜「-128.70」)与第 335 段(:335,搜「annotationDiameter_mm / 4」)。候选 551066 行、真结节 1351;两文件坐标差零点几毫米;模糊匹配:三轴差都不超直径/4;「真实数据集常常不完美,这就是把不同来源的数据拼起来时要做的工作」。 2 3 4 5

  7. 出处:「12 Combining data sources into a unified dataset」第 491 段(text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:491,搜「clip(-1000, 1000」)、第 666 段(:666,搜「Flip the coordinates」)、第 744 段(:744,搜「getRawCandidate」)与第 894 段(:894,搜「LunaDataset()[0]」)。clip 的理由:离群值会带歪 batch normalization 的统计;坐标转换四步原文在 12.4.3;样本四元组实例:(张量, tensor([0,1]), series_uid, tensor([91,360,341]))。 2

  8. 出处:「12 Combining data sources into a unified dataset」第 821 段(text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:821,搜「two functions」)与第 997 段(:997,搜「is only present either in training or in testing」)。Dataset 必须实现 lengetitem;「按任务,可能要保证同一病人的数据只在训练或只在测试」。 2

  9. 出处:「12 Combining data sources into a unified dataset」第 299 段(text/20-ch12-12-combining-data-sources-into-a-unified-dataset.txt:299,搜「on-disk caching」)与第 920 段(:920,搜「easily 50 times」)。内存缓存整张 CT + diskcache 落盘小块;「没有缓存,LunaDataset 慢 50 倍都不止」;改函数定义要清缓存。 2