本章涵盖以下内容:
- 将一个大型问题拆解为多个更小、更容易处理的问题
- 探索一个复杂的深度学习问题,并决定其结构与解决思路
- 下载训练数据
本章我们有两个主要目标。首先,我们将介绍本书第二部分的整体规划,以便我们对后续各章所要逐步构建的更大图景有一个扎实的认识。在第 12 章中,我们将开始构建数据解析和数据处理流程,这些流程会产出供第 13 章使用的数据,并在那一章中训练我们的第一个模型。为了把接下来这些章节所需的工作做好,本章还会介绍我们项目所处的一些背景环境:我们将讨论数据格式与数据来源,并探究问题领域对我们施加的约束。请习惯于执行这些任务,因为对于任何严肃的深度学习项目来说,你都必须这样做。
11.1 用例介绍
本书第二部分的目标,是为你提供一套工具,帮助你应对“事情不按预期运作”的情况——而这种情况,其实远比前几章让你感觉到的要常见得多。我们无法预测每一种失败情形,也无法覆盖每一种调试技巧,但我们希望,至少能为你提供足够多的方法,让你在遇到新的障碍时不至于束手无策。同样地,我们也希望帮助你避免这样一种局面:在自己的项目表现不佳时,你完全不知道下一步还能做什么。相反,我们希望到那时你脑中的可行思路多到列成清单后,真正的难题会变成如何排序优先级!
为了呈现这些想法和技巧,我们需要一个足够复杂、也有一定分量的背景场景。我们选择的是:仅以病人胸部的计算机断层扫描(CT)作为输入,自动检测肺部恶性肿瘤。我们会更关注技术挑战,而不是其对人的影响,但请不要误会——哪怕只是从工程角度来看,这个项目也比我们之前做过的任务需要更严肃、更有结构的方法,才有可能成功。
注意:CT 扫描本质上就是三维 X 光图像,可表示为一个由单通道数据构成的三维数组。我们很快会更详细地介绍它。
正如你可能已经猜到的那样,本章标题更多是为了吸引注意力,并带有夸张意味,而绝非什么严肃的宣言。我们准确地说:本书第二部分中的项目,将以人体躯干的三维 CT 扫描作为输入,并输出可疑恶性肿瘤的位置——如果它们存在的话。
尽早检测肺癌会对生存率产生巨大影响,但人工完成这项工作非常困难,尤其是在任何真正全面、面向全体人群的意义上。当前,审阅这些数据的工作必须由经过高度专业训练的专家来完成,这需要极其细致的注意力,而且大量病例实际上并不存在癌症。
要把这项工作做好,就像把你放在 100 个草堆前,然后告诉你:“判断这些草堆里是否有针,以及哪些草堆里有针。” 以这种方式进行搜索,很容易漏掉预警信号,尤其是在疾病早期、迹象更为细微的时候。人脑天生就不适合这种单调重复的工作。而这,当然就是深度学习可以发挥作用的地方。
将这一过程自动化,会让我们获得在“不太合作”的环境中工作的经验:在这样的环境里,我们必须更多地从零开始做事,而且面对可能遇到的问题时,可直接拿来用的答案也更少。不过,只要我们一起推进,最终一定能做到!当你读完本书第二部分时,我们认为你就已经准备好开始着手处理一个由你自己选择的、真实世界中尚未解决的问题了。
我们选择“肺部肿瘤检测”这个问题,有几个原因。首要原因是:这个问题本身还没有被解决!这一点很重要,因为我们想清楚地表明,你完全可以使用 PyTorch 去有效地攻克最前沿的项目。我们希望这能增强你对 PyTorch 作为框架的信心,也增强你对自己作为问题解决者的信心。
这个问题领域还有一个很好的特点,那就是虽然它尚未被解决,但近来已有很多团队在关注它,并且取得了颇具希望的结果。因此,这个挑战大致处在我们整体能力的边界附近;我们不会把时间浪费在一个距离出现合理解决方案还要几十年的问题上。人们对这一问题的关注,也带来了大量高质量论文和开源项目,这些都是绝佳的灵感与思路来源。等到本书第二部分结束后,如果你有兴趣继续改进我们做出的解决方案,它们会提供巨大的帮助。
本书第二部分会始终聚焦于“肺部肿瘤检测”这个问题,但我们要教授的技能是通用的。无论你在做什么项目,学会如何调查、预处理并组织数据以供训练,都是非常重要的。虽然我们会在肺部肿瘤这个特定背景下讲解预处理,但一般性的原则是:如果你想让项目成功,你就必须准备好做这样的工作。同样地,搭建训练循环、获取正确的性能指标,以及把项目中的多个模型整合成最终应用,也都是通用技能,而这些正是我们在第 11 到第 15 章中会依次使用的能力。
注意:虽然本书第二部分最终会得到一个可以运行的结果,但其输出精度还不足以用于临床。我们是把这个主题当作一个用于教授 PyTorch 的激励性示例,而不是把所有可能的技巧都用上去彻底解决这个问题。
11.2 为大型项目做准备
这个项目将建立在第一部分中学到的基础技能之上。尤其是,第 8 章关于模型构建的内容会直接相关。重复堆叠卷积层,再接上用于降低分辨率的下采样层,依然会构成我们模型的大部分结构。我们会使用三维数据作为模型输入。从概念上说,这与第 7 章和第 8 章中使用的二维图像数据类似,但我们将无法依赖 PyTorch 生态系统中那些专门面向二维数据的全部工具。
我们在第 8 章中使用卷积模型所做的工作,与本书第二部分将要做的工作之间,主要差异在于:我们会把多少精力投入到“模型本身以外”的事情上。在第 8 章中,我们使用了一个现成的数据集,在把数据送入模型进行分类之前,几乎没有进行什么数据处理。那时,我们几乎把所有时间和注意力都花在构建模型本身上;而现在,直到第 13 章,我们甚至都不会开始设计两个模型架构中的第一个。这是一个直接后果:我们面对的是非标准数据,没有现成的库替我们准备好可以直接喂给模型的训练样本。而在实践中,大多数真实项目都是如此。我们必须先去了解数据,并亲手实现不少东西。
即便完成了这些准备工作,这个项目最终也不会是那种“把 CT 转成张量,送进神经网络,答案就自动从另一头出来”的情况。正如许多真实世界中的案例一样,一个可行的方法往往会更复杂,因为我们需要考虑各种混杂因素,例如数据量有限、计算资源有限,以及我们设计有效模型能力上的限制。当我们逐步构建项目架构的高层说明时,请始终记住这一点。
说到有限的计算资源,本书第二部分需要使用 GPU 才能获得合理的训练速度,最好是拥有至少 8 GB 显存的 GPU。如果尝试在 CPU 上训练我们将要构建的模型,可能需要数周时间!(我们是这么推测的——我们既没真试过,也没做过计时。)如果你手头没有 GPU,第 15 章会提供预训练模型;那一章里的肺结节分析脚本,大概可以在一夜之间跑完。虽然我们不想在没有必要的情况下让本书依赖专有服务,但还是要提一下:在写作本书时,Colaboratory(colab.research.google.com)提供免费的 GPU 实例,可能会派上用场。而且,PyTorch 甚至已经预装好了!另外,你还需要至少 220 GB 的可用磁盘空间,用于存放原始训练数据、缓存数据以及训练好的模型。
注意:本书第二部分中给出的许多代码示例,都省略了一些会使事情复杂化的细节。为了避免让日志、错误处理以及边界情况把示例弄得杂乱无章,书中正文只保留表达核心思想的代码。完整可运行的代码示例可以在本书官网(www.manning.com/books/deep-…)以及 GitHub(github.com/deep-learni…)上找到。
好,我们已经明确这是一个困难而多面的复杂问题。那么,我们要怎么处理它呢?我们的做法不是去直接查看整张 CT 扫描,以寻找肿瘤或其潜在恶性特征,而是把它拆解成一系列更简单的问题,这些问题组合起来后,就能给出我们感兴趣的端到端结果。就像工厂流水线一样,每一步都会接收原材料(数据)和/或前一步骤的输出,进行处理,然后再把结果传给下一个环节。
并不是每一个问题都必须这样来解决,但把大问题拆成若干块、分别独立求解,往往是一个很好的起点。即便后来发现对于某个具体项目来说,这不是最优方式,我们在分块处理的过程中,也很可能已经学到了足够多的东西,从而大致知道如何把思路重构成真正可行的方案。
在进入如何拆解这个问题的具体细节之前,我们需要先了解一些医学领域的背景。代码清单会告诉你我们在做什么,但了解放射肿瘤学,才能解释我们为什么要这样做。理解问题空间,无论处于哪个领域,都是至关重要的。深度学习很强大,但它不是魔法,盲目地把它应用到非平凡问题上,几乎肯定会失败。我们必须把对领域空间的洞见,与对神经网络行为的直觉结合起来。然后,通过有纪律的实验与迭代改进,才能逐步逼近一个可行的解决方案。
11.3 CT 扫描到底是什么?
在项目推进得太远之前,我们需要花一点时间解释一下什么是 CT 扫描。由于在这个项目中,我们会大量使用 CT 扫描数据作为主要数据格式,因此,对这种数据格式的优势、弱点以及本质特性有一个可操作的理解,将是我们能否有效利用它的关键。我们之前提到的关键点是:CT 扫描本质上就是三维 X 光图像,可表示为一个由单通道数据组成的三维数组。正如我们可能还记得第 4 章所说的,它就像是一叠堆起来的灰度 PNG 图像。
VOXEL(体素)
体素(voxel)是我们熟悉的二维像素(pixel)在三维空间中的对应物。它包围的是一个体积(因此叫 volumetric pixel,体积像素),而不是一个面积,并且通常会排列成三维网格来表示一片数据场。三个维度中的每一个都对应一个可测量的距离。很多时候,体素是立方体,但在本章中,我们处理的体素是长方体。长方体由三个维度定义:长、宽和高,分别表示三维空间中三个轴向上的实际距离。与所有边都相等的立方体不同,长方体在比例上可以各不相同。
除了医学数据之外,我们还能在流体模拟、由二维图像重建三维场景、自动驾驶汽车使用的激光雷达(LIDAR)数据,以及许多其他问题领域中看到类似的体素数据。这些领域各自都有其特殊之处与细微差别。虽然我们将在这里介绍的 API 具有一般适用性,但如果想高效使用这些 API,我们也必须了解与之配套的数据本身是什么样的。
CT 扫描中的每个体素都有一个数值,它大致对应于其中所包含物质的平均质量密度。对这种数据的大多数可视化方式,会把高密度物质(例如骨骼和金属植入物)显示为白色,把低密度的空气和肺组织显示为黑色,而把脂肪和其他组织显示为不同深浅的灰色。再次强调,这在视觉上与 X 光图像有些相似,但也存在几个关键差别。
CT 扫描与 X 光最主要的区别在于:X 光是把三维强度信息(此处指组织和骨骼密度)投影到二维平面上,而 CT 扫描则保留了数据的第三个维度。这使得我们可以用多种方式来渲染数据——例如,将其渲染为一个灰度实体,正如图 11.1 所示。
图 11.1 一个人体躯干的 CT 扫描,图中从上到下可见皮肤、器官、脊柱以及患者支撑床。来源:mng.bz/04r6;Mindwa… CT Software / CC BY-SA 3.0。
注意:CT 扫描实际测量的是放射密度(radiodensity),它同时受到质量密度和被测材料原子序数的影响。对我们这里的目的而言,这一区别并不重要,因为无论输入的确切单位是什么,模型都会直接消费并从 CT 数据中学习。
这种三维表示还允许我们通过隐藏不感兴趣的组织类型,来“看到”被扫描对象的内部。例如,我们可以对数据进行三维渲染,并把可见内容限制在骨骼和肺组织上,就像图 11.2 那样。
图 11.2 一张显示肋骨、脊柱和肺部结构的 CT 扫描图
CT 扫描比 X 光更难获取得多,因为它需要像图 11.3 中所示那样的设备。这类设备通常价值超过一百万美元,而且还需要受过训练的专业人员来操作。大多数医院以及部分设备完善的诊所都配有 CT 扫描仪,但它们远不像 X 光设备那样普及。再加上病人隐私方面的监管要求,除非已经有人完成了数据收集与整理,否则要拿到一批 CT 扫描数据并不容易。
图 11.3 一位患者位于 CT 扫描仪内部,图中还叠加显示了 CT 扫描的边界框。除库存宣传照外,患者通常不会穿着日常衣物进入机器。
图 11.3 还展示了一个示例边界框,表示 CT 扫描所覆盖的区域。患者躺着的床会前后移动,使扫描仪能够对患者进行多层切片成像,从而填满这个边界框。扫描仪中央较暗的环形区域,正是实际成像设备所在的位置。
CT 扫描与 X 光之间的最后一个差异是:CT 数据是纯数字格式。扫描过程的原始输出对人眼来说并没有太大意义,必须通过计算机正确重解释之后,才能变成我们可以理解的形式。而且,扫描时 CT 扫描仪的设置,会对最终产生的数据造成很大影响。
虽然这些信息看上去似乎不算特别相关,但实际上我们已经学到了一些东西。从图 11.3 中,我们可以看到,患者实际上是沿着头到脚的方向在移动。因此,CT 扫描仪在该轴向上的距离测量方式,与另外两个轴是不一样的。这解释了(或者至少强烈暗示了)为什么我们的体素未必是立方体,也与我们将在第 14 章中如何处理数据有关。这正是一个很好的例子,说明如果我们想有效地解决问题,就必须理解问题空间本身。当你开始处理自己的项目时,也务必要对数据细节进行同样深入的调查。
11.4 项目:一个端到端的肺癌检测器
现在我们已经对 CT 扫描的基础有了大致认识,接下来讨论项目结构。磁盘上绝大多数字节都将用于存储 CT 扫描的三维数组,这些数组包含密度信息;而我们的模型主要会消费这些三维数组中的各种子切片。为了从“查看整个胸部 CT 扫描”走到“给患者做出肺癌诊断”,我们将使用三个主要步骤。
我们的完整端到端方案如图 11.4 所示:它会加载 CT 数据文件,生成一个 Ct 实例,其中包含完整的三维扫描数据;然后再结合一个执行分割(segmentation)的模块,该模块会标记出感兴趣的体素。
图 11.4 从整张胸部 CT 扫描出发,最终判断患者是否存在恶性肿瘤的端到端流程。
NODULES(结节)
肺部中由增殖细胞形成的一团组织称为肿瘤(tumor)。肿瘤可以是良性的,也可以是恶性的;若为恶性,也就是癌症。肺部中较小的肿瘤(宽度只有几毫米)称为结节(nodule)。大约 40% 的肺结节最终会被证实为恶性的小型癌症。尽可能早地发现这些结节非常重要,而这依赖于我们这里正在讨论的这类医学影像。
结节的位置会再与 CT 的体素数据结合,生成结节候选区域(nodule candidates),然后这些候选区域可以交给我们的结节分类模型,判断它们首先是否真的是结节,进一步再判断它们是否是恶性的。后一项任务尤其困难,因为仅凭 CT 影像,恶性程度未必能清晰显现出来,不过我们会看看能走多远。最后,这些针对单个结节的分类结果,可以再综合起来,形成对整位患者的整体诊断。
更具体地说,我们将执行以下工作:
-
将原始 CT 扫描数据加载为可供 PyTorch 使用的形式。
把原始数据转换成 PyTorch 能使用的形式,是你面对任何项目时的第一步。对于二维图像数据,这个过程要稍微简单一些;对于非图像数据,则更简单一些。 -
使用 PyTorch 识别肺部中潜在肿瘤所在的体素,实现一种称为分割(segmentation)的技术。
这大致相当于生成一张“热力图”,指出哪些区域应当送入第 3 步中的分类器。这样一来,我们就可以把注意力集中在肺内潜在的肿瘤区域,而忽略大面积不相关的解剖结构(例如,一个人不可能在胃里得肺癌)。一般来说,在学习过程中,能够专注于一个单独且较小的任务是最好的。随着经验增长,在某些场景中,更复杂的模型结构确实能够产生极佳的结果;但若想从零设计这类结构,首先必须牢牢掌握基础构件。
-
使用 3D 卷积将候选结节分类为真正的结节或非结节。
这在概念上与我们在第 8 章讲过的二维卷积类似。决定一个候选结构究竟是什么性质的特征,主要都集中在该肿瘤局部附近,因此这种方式能够在“限制输入数据大小”与“保留相关信息”之间取得较好平衡。像这样做出“限制问题范围”的决策,有助于让每个独立任务都保持受控,从而在排查问题时减少需要检查的东西。
站在巨人的肩膀上
我们之所以采用这种三步方案,是因为我们站在巨人的肩膀上。并没有任何特别的理由让我们能够事先知道,这样的项目结构就一定适合这个问题。相反,我们依赖的是那些真正实现过类似系统并报告过成功经验的人。预计当你切换到不同领域时,仍然需要通过实验来找到可行的方法;但始终要尽可能从这个领域中已有的努力、以及那些在相似领域中工作并发现了可迁移经验的人那里学习。走出去,看看别人做了什么,把它作为基准。同时,也不要只是盲目拿来代码运行,因为你必须真正理解自己正在运行的代码,才能利用它的结果推动自己的项目取得进展。
图 11.4 只描绘了当我们构建并训练好所有必需模型之后,整个系统的最终路径。为了训练这些相关模型而实际需要做的工作,会随着我们逐步接近每个步骤的实现而展开。
我们来回顾一下要走的步骤:
- 第 1 步——数据加载
- 第 2 步——分割
- 第 3 步——分类
我们用于训练的数据,为第 3 步提供了人工标注的输出。因此,我们可以把第 2 步(识别相关体素)与第 3 步(结节候选分类)几乎看作两个相互独立的项目。既然人类专家已经为数据标注了结节位置,那么我们就可以按自己喜欢的顺序去处理第 2 步或第 3 步。
我们会先做第 1 步,然后直接跳到第 3 步,之后再回来实现第 2 步。原因在于,第 3 步需要的方法与第 8 章中做过的事情更相似:使用多层卷积与池化层来聚合空间信息,再送入线性分类器。一旦我们对分类模型有了把握,就可以开始处理第 2 步。由于分割是一个更复杂的话题,我们希望在不必同时学习“分割本身”以及“CT 扫描和恶性肿瘤的基础知识”的情况下来攻克它。换言之,我们会先在一个更熟悉的分类问题中探索癌症检测这一领域。
这种“从问题中间开始,向两边展开”的做法,看起来可能有点奇怪。直觉上,从第 1 步开始一路顺着往前走似乎更合理。然而,能够把问题切开并独立地处理各步骤是很有用的,因为这会鼓励更模块化的解决方案;此外,也更容易在一个小团队内部拆分工作量。而且,实际临床使用者也很可能更偏好一种“标记可疑结节供人工复查”的系统,而不是直接给出一个单一的二元诊断结果。相比于从一开始就做一个整体式、自顶向下的系统,这种模块化方案更容易适配不同用例。
在逐步实现每一步的过程中,我们会相当详细地讨论肺部肿瘤,也会呈现大量关于 CT 扫描的细粒度细节。对于一本聚焦 PyTorch 的书来说,这似乎有点偏题,但我们之所以这么做,正是为了让你开始形成对问题空间的直觉,也因为这正是现实中从业者的工作方式。之所以必须具备这些临床信息,是因为可选的解决方案和思路空间实在太大了,不可能把它们全部都编码、训练并评估一遍。
如果我们面对的是另一个项目(比如你读完本书后要做的那个项目),我们仍然需要做调查,去理解数据与问题空间。假设你对卫星测绘感兴趣,而你的下一个项目需要处理从轨道上拍摄的地球图像。那么你就必须问一些问题:采集到的是哪些波段?只有普通的 RGB,还是更特别的东西?有没有红外?有没有紫外?此外,图像还可能受到拍摄时间的影响,或者因为目标地点并不正好位于卫星正下方而导致透视偏斜。图像是否需要校正?
即便你假想中的第三个项目所用的数据类型没有变,所处领域也很可能会改变很多事情,甚至是彻底地改变。比如,处理自动驾驶汽车摄像头输出的数据,仍然是二维图像,但其中的复杂性与注意事项却截然不同。例如,测绘卫星就不太需要担心太阳直射镜头,或者镜头上沾了泥巴!
我们必须能够用自己的直觉来引导对潜在优化与改进方向的调查。这对深度学习项目而言普遍如此,而我们会在本书第二部分中不断练习如何运用这种直觉。那么,现在就开始吧。先退一步,做一次直觉检查。你的直觉对这种方法怎么看?你会觉得它过于复杂了吗?
11.4.1 为什么我们不能只是不断往神经网络里塞数据,直到它起作用?
读完上一节之后,你可能会疑惑:为什么我们需要两种彼此独立的模型架构?为什么整个数据流如此复杂?这是有原因的。我们的思路与第 8 章不同,是因为这个任务很难自动化,而且人们至今还没有完全解决它。这种困难会直接转化为复杂性;一旦我们整个社会真正彻底解决了这个问题,大概就会有现成的库包可以直接拿来用,让它“开箱即用”。但现在,我们还没有走到那一步。
那为什么会这么难呢?首先,CT 扫描中的绝大多数内容,对于回答“这个病人是否有恶性肿瘤?”这个问题来说,根本就不重要。这一点从直觉上也说得通,因为病人体内的绝大部分组织都是健康细胞。即使在存在恶性肿瘤的病例中,CT 中高达 99.9999% 的体素也依然不会是癌变组织。这个比例,相当于在一台高清电视屏幕上,某个地方只有两个颜色略有偏差的像素;或者相当于在一整排小说书架中,只有一个单词拼写错误。
你能在图 11.5 的三个视图中找出那个被标记为结节的白点吗?(这个样本的 series_uid 是 1.3.6.1.4.1.14519.5.2.1.6279.6001.126264578931778258890371755354,如果你之后想更仔细地查看它,这个信息会有用。)
图 11.5 一张 CT 扫描中,大约有 1,000 个结构在未经训练的人眼看来都像肿瘤。经由人工专家审阅后,其中恰好只有一个被识别为结节。其余的都是正常的解剖结构,例如血管、病灶以及其他无害的小块组织。
如果你需要提示,可以利用索引、行、列这几个值来帮助定位相关的高密度组织小块。你觉得自己能够仅凭这样的图像(而且仅仅是图像——没有索引、行、列信息!)推断出肿瘤的相关特征吗?如果给你的是完整的三维扫描,而不是仅仅三张穿过感兴趣区域的切片呢?
注意:如果你找不到那个肿瘤,也不必焦虑!我们就是想说明:这类数据是多么微妙。正因为它在视觉上很难识别,这个例子才成立。
你可能在别处看到过:在一般计算机视觉任务中,端到端的目标检测与分类方法通常非常成功。torchvision 中也包含诸如 Fast R-CNN / Mask R-CNN 这样的端到端模型,但这些模型通常是在数十万张图像上训练出来的,而且它们的数据集也不像我们这里这样受限于稀有类别样本数量不足。
我们即将采用的项目架构,其优势在于:在数据量比较有限的情况下,它也能较好工作。所以,虽然从理论上说,确实可以不断往神经网络里塞任意多的数据,直到它既学会发现那根“丢失的针”,又学会忽略那堆“草”,但在实践中,收集足够多的数据、再等待足够久的训练时间,代价会大到难以承受。这并不是最好的方法,因为效果并不理想,而且大多数读者也根本没有足够的算力资源去做到这一点。
为了提出最佳方案,我们当然也可以研究那些已经被证明能够更好地实现端到端数据整合的模型设计,比如 Retina U-Net(arxiv.org/pdf/1811.08…)和 FishNet(mng.bz/K240)。这些复杂设计确实能够产出高质量结果,但它们并不适合当前这个场景,因为要理解其背后的设计决策,前提是你已经掌握了基础概念。也就是说,这些高级模型并不适合一边讲授基础知识、一边拿来作为教学示例!
这并不是说,我们的多步骤设计就是最好的方法;只是因为“最好”始终取决于我们用什么标准去评估。正如一个项目可能有很多不同目标一样,也可能存在很多种“最好”的方案。我们这种自包含的多步骤方法,也有一些缺点。
回想一下第 2 章中的 GAN 游戏。在那里,我们有两个网络合作来生成逼真的古典大师风格伪作。艺术家网络会创作出一个候选作品,学者网络则对其进行点评,并给艺术家提供如何改进的反馈。用技术术语来说,模型结构允许梯度从最终的分类器(真假判别)一路反向传播到项目的最前端部分(艺术家)。
而我们现在用于解决问题的方法,并不会利用端到端的梯度反向传播,直接围绕最终目标进行优化。相反,我们会分别优化问题中的离散部分,因为我们的分割模型和分类模型不会一起联合训练。这样做可能会限制最终方案的上限效果,但我们认为,它会带来更好的学习体验。
我们认为,能够一次只专注于一个步骤,会让我们得以放大视野并集中精力学习较少数量的新技能。我们的两个模型都只专注于执行一项任务。就像人类放射科医生在一张一张地查看 CT 切片时一样,如果工作范围被清晰限定,那么训练起来就会容易得多。我们还希望提供一些工具,以便对数据进行更丰富的操作。能够放大并聚焦某个具体位置的细节,在模型训练期间会极大提升整体效率;相比之下,如果总是必须一次查看整张图像,效率会低得多。我们的分割模型被迫要消费整张图像,但我们会把系统设计成:分类模型获得的是那些感兴趣区域的局部放大视图。
第 3 步(分类)将消费的数据,会类似于图 11.6 所示的图像:那是肿瘤的一系列横断面切片。这张图展示的是一个可能为恶性(或至少说性质未定)的肿瘤的近景。对于未经训练的人眼(或者未经训练的卷积网络)来说,这团组织可能看起来平平无奇;但至少,相比起必须处理我们前面看到的整张 CT,这已经是一个被大大限制范围的问题——任务变成了判断这样一个局部结构究竟是良性还是恶性。
图 11.6 对图 11.5 中 CT 扫描里的肿瘤所做的多切片近景裁剪图。
注意:下一章中的代码会提供一些例程,用来生成像图 11.6 这样的局部放大的结节图像。
我们会在第 12 章完成第 1 步的数据加载工作,第 13 章和第 14 章将聚焦于解决这些结节的分类问题。之后,我们会退回去处理第 2 步(利用分割找出候选肿瘤)——这部分将在第 15 章中完成。
注意:CT 的标准显示方式,会把 superior(也就是接近头部的一端)放在图像上方,但 CT 数据在存储切片顺序时,第一张切片却是 inferior(也就是朝向脚部的一端)。因此,如果我们不加处理,Matplotlib 渲染出来的图像会是上下颠倒的。由于这种翻转对模型本身并没有实质影响,我们不会让原始数据到模型之间的代码路径变得更复杂;但在渲染代码中,我们会加上翻转,让图像以正确方向显示。
WHAT IS A NODULE?(什么是结节?)
正如我们一直在说的,要想足够了解数据,以便有效利用它,我们就必须学习一些关于癌症和放射肿瘤学的具体知识。最后一个需要理解的关键概念,就是“结节”到底是什么。简单来说,结节是一个统称,用来指肺内可能出现的各种大小不一的隆起和团块。其中有些从患者健康角度看是有问题的,有些则不是。更精确的定义(见 Eric J. Olson,《Lung Nodules: Can They Be Cancerous?》,Mayo Clinic,mng.bz/yyge)把结节的尺寸上限定为 3 cm,再大的团块则称为肺部肿块(lung mass)。不过,我们会把“结节”这个词泛指所有这类解剖结构,因为 3 cm 这个界限多少带有一点任意性,而我们将用同样的代码路径去处理大于和小于 3 cm 的团块。一个结节最终可能是良性的,也可能是恶性肿瘤(也就是癌症)。从放射学角度看,结节其实与其他由多种原因造成的团块非常相似:感染、炎症、供血问题、异常血管,以及肿瘤以外的其他疾病,都可能形成类似外观。
关键在于:我们要检测的癌症,总是会以结节的形式出现,要么悬浮在密度很低的肺组织中,要么附着在肺壁上。这意味着,我们可以把分类器的输入范围限制在结节上,而不必让它检查所有组织。能够缩小预期输入的范围,将有助于分类器学习当前这项任务。
这再次说明:我们要使用的底层深度学习技术是通用的,但不能盲目套用——至少如果我们想获得像样的结果,就绝不能这么做。我们必须理解自己所工作的领域,才能做出真正对项目有帮助的选择。
在图 11.7 中,我们可以看到一个典型的恶性结节示例。我们关心的最小结节,直径只有几毫米,虽然图 11.7 中这个结节要更大一些。正如本章前面讨论过的,这意味着最小的结节大约比整个 CT 扫描小一百万倍。根据美国国家癌症研究所(National Cancer Institute)癌症术语词典(mng.bz/jgBP),在患者体内发现的结节中,超过一半都不是恶性的。
图 11.7 一张 CT 扫描图,其中一个恶性结节在视觉上与其他结节存在差异
11.4.2 我们的数据来源:LUNA Grand Challenge
我们刚才看到的 CT 扫描,来自 LUNA(LUng Nodule Analysis)Grand Challenge。LUNA Grand Challenge 将一个带有高质量标签的公开病人 CT 扫描数据集(其中许多含有肺结节)与一个基于这些数据对分类器进行公开排名的榜单结合在了一起。在医学研究与分析领域,公开共享数据集已经形成了一定文化;对这类数据的开放访问,使研究人员能够在无需机构间正式研究协议的前提下使用、组合并开展新的研究(当然,也有一些数据会保持私有)。LUNA Grand Challenge 的目标,是通过让团队能够便捷地参与排行榜竞争,来推动结节检测能力的提升。项目团队可以用标准化标准(即所提供的数据集)来测试自己检测方法的有效性。若要进入公开排名,团队必须提交一篇科学论文,描述项目架构、训练方法等内容。LUNA Grand Challenge 是一个极佳的资源,可以为后续项目改进提供更多灵感和思路。
注意:许多野外真实场景中的 CT 扫描数据,往往在不同扫描仪与处理程序之间存在大量混乱和不一致之处。例如,有些扫描仪会把 CT 扫描视野之外的区域,标记成负值密度。CT 扫描还可以采用各种不同的扫描参数进行采集,而这些参数会以从细微到极其剧烈的方式改变最终图像。虽然 LUNA 数据整体上比较干净,但如果你打算引入其他数据源,一定要检验自己的各种假设。
我们将使用的是 LUNA 2016 数据集。LUNA 网站(luna16.grand-challenge.org/Description)描述了该挑战赛的两个赛道:第一个赛道“Nodule detection (NDET)”大致对应于我们的第 1 步(分割);第二个赛道“False positive reduction (FPRED)”则与我们的第 3 步(分类)相似。当网站谈到“possible nodules 的位置”时,它指的就是一种与我们将在第 15 章中介绍的方法类似的过程。
11.4.3 下载 LUNA 数据
在进一步深入我们项目的技术细节之前,我们先来介绍如何获取要使用的数据。压缩后的数据大约有 60 GB,因此根据你的网络连接速度,下载可能需要一些时间。解压后会占用大约 120 GB 空间,而我们还需要另外约 100 GB 的缓存空间,用于存放更小的数据块,以便比起每次都读取整个 CT,可以更快地访问它们。所需缓存空间是按章节分别计算的,不过一旦你完成某一章,就可以删除该章缓存来释放空间。
请访问:luna16.grand-challenge.org/Download/ 。你会看到两个指向 Zenodo 数据的下载链接。你需要从这两个链接中都下载数据。
我们要使用的数据分为 10 个子集,名字很直接,就叫 subset0 到 subset9。把每一个压缩包都解压,这样你就会得到类似 code/data-unversioned/part2/luna/subset0 这样的独立子目录,其他子集也是如此。在 Linux 上,你需要用到 7z 解压工具(Ubuntu 可通过 p7zip-full 软件包安装)。Windows 用户可以从 7-Zip 官网(<www.7-zip.org>)获取解压工具。有些解压程序无法打开这些压缩包;如果你报错了,请确认自己安装的是完整版本的解压工具。
此外,你还需要 candidates.csv 和 annotations.csv 这两个文件。为了方便起见,我们已经把它们放在本书官网和 GitHub 仓库中,因此它们应该已经存在于 code/data/part2/luna/*.csv 路径下。你也可以从与数据子集相同的位置下载这两个文件。
注意:如果你没有方便获取大约 220 GB 可用磁盘空间,那么也可以只使用 10 个子集中的 1 个或 2 个来运行示例。更小的训练集会导致模型性能显著变差,但总比完全无法运行示例要好。
一旦你拿到了 candidates 文件,并且至少下载、解压并正确放置了一个数据子集,你就应该能够开始运行下一章中的示例了。
11.5 结论
我们已经朝项目迈出了重要的一步!你也许会觉得,我们似乎并没有真正做成什么——毕竟,到现在为止,我们连一行代码都还没实现。但请记住,当你将来独立做项目时,也同样需要像我们在这里所做的这样,先进行研究与准备。
在本章中,我们试图完成两件事:
- 理解围绕肺癌检测项目的更大背景
- 为本书第二部分勾勒出项目的方向与整体结构
如果你仍然觉得我们还没有取得真正进展,请认识到:这种心态其实是一种陷阱——理解项目所处的空间至关重要,而我们已经完成的设计工作,会在后续推进中带来丰厚回报。一旦我们在第 12 章中开始实现数据加载流程,你很快就会看到这些回报。
由于本章纯属信息性内容,没有任何代码,因此我们这里暂时跳过练习部分。
小结
- 我们检测癌性结节的方法,大致分为三个步骤:数据加载、分割和分类。
- 将项目拆解成多个较小、半独立的子项目,会让分别讲解这些子项目变得更容易。对于未来目标不同的其他项目,也许会有更合适的方法。
- 一次 CT 扫描是一个三维强度数据数组,通常包含大约 3200 万个体素,而我们想识别的结节大约比它小一百万倍。让模型只关注与当前任务相关的 CT 局部裁剪区域,会更容易通过训练获得合理结果。
- 理解我们的数据,会让我们更容易编写合适的数据处理流程,避免扭曲或破坏数据中的重要特征。CT 扫描数组中的体素通常并不是立方体;将现实世界单位下的位置信息映射到数组索引时,需要进行换算。CT 扫描的强度值大致对应质量密度,但使用的是一套特殊单位。
- 识别项目中的关键概念,并确保它们在设计中得到充分体现,可能至关重要。我们项目的大部分内容都围绕“结节”展开。结节是肺中的小块组织团,在 CT 上,它们会与许多外观相似的其他结构一起出现。
- 我们将使用 LUNA Grand Challenge 数据来训练模型。LUNA 数据包含 CT 扫描,以及用于分类与分组的人类标注输出。高质量数据会对项目成功产生显著影响。