医疗保健专业机器学习实践指南(二)

原文:Practical AI for Healthcare Professionals

协议:CC BY-NC-SA 4.0

六、项目 2:中枢神经系统和胸部 x 光肺炎检测

本章的所有支持代码可在 https://github.com/Apress/Practical-AI-for-Healthcare-Professionals/tree/main/ch6 找到

现在,我们已经了解了基本的机器学习算法及其工作原理,让我们转向神经网络。在这一章中,我们将应对图像分类的挑战:尝试使用胸部 x 射线来检测患者的肺炎(即,为每张图像指定“肺炎”或“正常”状态)。我们还将看到,当评估图像时,我们如何可视化神经网络“关注”什么(通过使用一种称为“Grad-CAM”的技术)。

项目设置

首先,我们需要为这项任务找到一个合适的数据集。幸运的是,一个名为 Kaggle 的网站定期举办机器学习比赛(其中一些与医学图像分类任务有关)。作为这些竞赛的一部分,组织(通常是公司,但也有政府机构,如 NIH)提供公共数据集供使用。本章我们将使用以下数据集: www.kaggle.com/paultimothymooney/chest-xray-pneumonia

本项目的相关任务如下:

  1. 使用 Kaggle API 将数据集下载到 Colab 笔记本中。

  2. 将数据分为训练集、验证集和测试集,并可视化正常病例和肺炎病例的分布。

  3. 为我们的每个数据子集创建数据生成器(您将在本章后面了解这是什么),并扩充我们的训练/验证图像。

  4. 为我们的图像分类任务创建一个名为“SmallNet”的小型神经网络。

  5. 设置网络“回调”以在训练过程中调整神经网络参数,并记录进度统计。

  6. 训练小网。

  7. 使用现有的神经网络(VGG16)并为我们的数据集定制它(通过一个称为“迁移学习”的过程)。

  8. VGG16 列车。

  9. 在测试集上使用 Grad-CAM 可视化两个模型的激活图,并评估两个模型。

记住所有这些,让我们开始吧!

Colab 设定

对于本章,请确保将运行时类型更改为“GPU”。在一个新的 Colab 笔记本中,转到运行时➤更改运行时类型,并在下拉菜单中将 CPU 或 TPU 更改为 GPU。

下载数据

首先,我们需要下载将要使用的图像。

我们将使用这个 Kaggle 数据集: www.kaggle.com/paultimothymooney/chest-xray-pneumonia

要快速下载数据集,您需要执行以下操作:

  1. 创建一个 Kaggle.com 帐户。

  2. 单击您的个人资料图片(在右上角)。

  3. 点击“账户”

  4. 向下滚动到“API”部分。

  5. 首先,单击“过期 API 令牌”确保弹出一个通知,说明 API 令牌已经过期或者不存在 API 令牌。

  6. 然后点击“创建新的 API 令牌”;这样做应该会生成一个 kaggle.json 文件,它会自动下载到您的计算机上。

  7. 将 kaggle.json 文件上传到 Google Colab。

  8. 运行以下代码行:

输入【单元格= 1】

!pip install kaggle
!mkdir /root/.kaggle
!cp kaggle.json /root/.kaggle/kaggle.json
!chmod 600 /root/.kaggle/kaggle.json
!kaggle datasets download -d paultimothymooney/chest-xray-pneumonia
!unzip chest-xray-pneumonia.zip
!rm -rf chest_xray/__MACOSX
!rm -rf chest_xray/chest_xray

输出有很多很多行文本不太相关,无法详细描述。然而,让我们来看看上面所有这些实际上都做了什么。

在前面的章节中,我们有时会在代码中使用!pip install somelibrary行。但是在这种情况下,!实际上在做什么呢?嗯,像pip ...这样的命令实际上并不是 Python 代码。它们是你通常会输入到命令提示符或终端的命令(这是你电脑上的一个独立程序,允许你只需输入几个关键字就可以访问文件和运行脚本)。pip install命令实际上会调用一个单独的程序从网上找到一个 Python 库,下载到你的电脑上,安装好就可以用了。在这里,我们正在安装kaggle Python 库,它允许我们以编程方式与 kaggle.com 网站进行交互。但是,当胸部 x 射线挑战页面有一个大按钮允许我们下载整个数据集时,为什么要做所有这些努力呢?这个数据集非常大(几千兆字节),下载到我们的电脑需要一段时间。此外,我们需要将数据集上传回 Colab 笔记本,这可能非常耗时。相反,我们可以使用 Kaggle Python 库将数据集自动下载到 Colab(或任何运行 Python 笔记本的地方)。

让我们浏览一下前面代码片段的所有其他行。

第 2 行将在位置/root/创建一个名为.kaggle的文件夹(注意,mkdir是“制作目录”的缩写,这是一种方便的方式来记住它是用来制作文件夹的,因为目录只是文件夹的另一个名字)。但是为什么要在这样的位置创建文件夹呢?Kaggle 的库实际上要求我们将 kaggle.json 文件(我们从他们的网站下载的)存储在.kaggle子文件夹下的/root文件中。另外,一个次要的注意事项:/root文件夹是一个存在于系统级的文件夹。与您交互的大多数文件夹都存在于用户级别(例如,桌面、下载、照片等。).

第 3 行将我们在 Colab 主目录中的 kaggle.json 文件复制到。/root 文件夹的 kaggle 子文件夹。它还确保它的名字仍然是那个位置的kaggle.json。请注意我们如何在这些命令中将位置指定为 folder/sub foldername/sub foldername/。这些被称为文件路径,是引用特定位置的便捷方式,而不是不断提到某个东西是另一个文件夹的子文件夹。还要注意,cp是 copy 的缩写(也是一种更容易记忆的方法)。

第 4 行修改 kaggle.json 文件的“权限”。文件权限是系统级的限制,允许我们确切地指定用户对典型文件可以做什么和不可以做什么。这里,我们将 kaggle.json 文件的文件权限设置为600,这意味着它可以被读取和写入。我们不会对文件本身进行任何写入,但最好将它保存在那里,以防您需要直接编辑文件。

第 5 行将调用 Kaggle 库命令行工具(Kaggle 库的一部分,可以从命令行而不是 Python 程序进行交互),并将下载与名称paultimothymooney/chest-xray-pneumonia匹配的数据集(注意完整命令中的-d用于指定应该下载与数据集相关联的文件)。

第 6 行用于解压上一步下载的 zip 文件。这将导致在主 Colab 目录中创建一个chest_xray文件夹。

第 7 行和第 8 行删除了chest_xray文件夹中的两个子文件夹__MACOSXchest_xray。无论如何,这些子文件夹的内容包含主数据集的副本,因此没有必要将它们保存在我们的 Colab 会话中。

拆分数据

在运行完这些命令后,您应该有一个名为“chest_xray”的文件夹,其中有三个子文件夹,分别名为“test”、“train”和“val”。如果您尝试查看各个文件夹,您会看到“train”文件夹有两个子文件夹,分别名为“NORMAL”和“PNEUMONIA”(与“val”和“test”相同)。“正常训练”和“肺炎”文件夹有许多图像(。jpg 文件),测试文件夹也是如此。然而,“val”文件夹在正常和肺炎文件夹中都只有一些图像。这些文件夹包含用于训练我们的神经网络的训练、验证和测试数据;然而,由于验证数据很少(只有 16 幅图像),我们最好自己尝试重新分割数据,以确保更好地分割训练、验证和测试数据。

为此,我们基本上要做到以下几点:

  1. 获取 chest _ xrays 文件夹中所有图像的路径(也称为文件位置)。

  2. 重新分割数据,以便 20%的数据用于测试。

  3. 通过在训练数据集和测试数据集中绘制正常与肺炎图像的频率,验证我们做的事情是正确的,并且没有扰乱数据的分布。

因此,应该注意确保我们的训练/测试部分是足够的。我们稍后将处理如何进行验证分割,但是让我们看看代码:

输入【单元格= 2】

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

from imutils import paths
from sklearn.model_selection import train_test_split

def generate_dataframe(directory):
  img_paths = list(paths.list_images(directory))
  labels = ['normal' if x.find('NORMAL') > -1 else 'pn' for x in img_paths]
  return pd.DataFrame({ 'paths': img_paths, 'labels': labels })

all_df = generate_dataframe('chest_xray')

train, test = train_test_split(all_df, test_size=0.2, random_state=42)

print(train['labels'].value_counts())
print(test['labels'].value_counts())

输出

pn        3419
normal    1265
Name: labels, dtype: int32
pn        854
normal    318
Name: labels, dtype: int32

前五行代码只是用来导入一些将帮助我们完成这项任务的库(以及其他库)。有一个我们以前没见过的新图书馆叫做imutils。它用于处理图像,并提供了一个名为paths的便捷模块,可以找到一个文件夹(及其所有子文件夹)中的所有图像。这对我们获取所有图像的列表非常有用。

一般来说,代码的结构是这样的,我们首先调用一个我们定义的叫做generate_dataframe的方法。这使得 pandas 数据框包含两列:一列包含所有图像的路径,另一列显示该图像的标签(正常或肺炎)。

然后,我们将使用 scikit-learn 的train_test_split方法分割数据,并指定我们希望将 20%的数据分配给测试集。最后,我们将打印出我们刚刚生成的训练和测试集中每个类的计数。让我们进一步看看generate_dataframe方法,因为我们以前没有遇到过这个方法。

generate_dataframe方法接受一个名为directory的参数。这被传递给paths.list_images方法,该方法以 Python 生成器的形式返回给我们(除了它可以生成一个行为类似 for 循环的函数之外,您实际上不需要知道这是什么)。然而,我们并不真的想要一个发电机;我们只是想要一个文件路径列表。为此,我们只需将paths.list_images调用包装在list()调用中,这将返回我们的图像路径,其中包含我们调用generate_dataframe的目录的所有子文件夹中的所有图像。

接下来,我们需要找到与这些图像相关联的实际标签。如前所述,每个图像都位于一个名为 NORMAL 或 PNEUMONIA 的文件夹中。所以,我们需要做的就是查看我们的每一个图像路径,看看“正常”这个词是否出现:如果出现,那就是一个正常的图像;否则就是肺炎形象。我们可以通过列表理解来做到这一点:

['normal' if x.find('NORMAL') > -1 else 'pn' for x in img_paths]

这基本上相当于

new_list = []
for x in img_paths:
    if x.find('NORMAL') > -1
        new_list.append('normal')
    else:
        new_list.append('pn')

小提醒:如果没有找到传递给.find的短语,字符串文件路径上的.find函数(暂存在变量x中)将返回-1;否则,它返回大于-1 的值。

现在,我们有了一个图像路径列表,还有一个标签列表。在一个有两列(一列为路径,一列为标签)的数据框中有这样的方法会很好,因为它与库中的其他方法配合得很好,我们将使用这些方法来构建我们的神经网络。要做到这一点,我们需要做的就是调用pd.DataFrame方法并传入一个字典,其中的键相当于列名,各自的值相当于组成这些列的列表。

然后我们可以调用generate_dataframe ('chest_xray')来获得所有图像的数据帧。train_test_split方法也将分割我们的数据框架(并试图确保两个类具有相同的标签分布)。最后,我们将通过调用'labels'列上的.value_counts()来打印出新生成的traintest数据帧中标签的频率。

我们还可以继续绘制计数:

输入【单元格= 3】

fig = plt.figure(figsize=(10,3))
for idx, x in enumerate([train, test]):
  fig.add_subplot(1,2,idx+1)
  x['labels'].value_counts().plot(kind='bar')

输出

参见图 6-1 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-1

培训和测试案例的分布

我们可以看到,正常与肺炎(这里称为“pn”)图像在训练和测试数据集中的分布基本相同。重要的是,我们看到肺炎数据相对于正常数据被过度表示,如果我们不考虑这一点,这可能会使我们的网络产生偏差。

现在我们已经创建了训练和测试数据集,我们可以开始实际使用神经网络构建库tensorflowkeras(包含在 TensorFlow 中)。我们将首先创建一个 ImageDataGenerator,它允许网络轻松地获取训练、验证和测试图像。

创建数据生成器和增强图像

数据生成器是我们的网络实际使用存储在数据框中的训练和测试数据的方式。由于我们的数据框架只包含图像和标签的路径,我们理想的情况是能够自动将这些图像读入计算机的内存,这样我们即将制作的神经网络程序就可以在其上进行学习(或对图像进行评估)。

我们正在使用的神经网络库使我们能够创建一个ImageDataGenerator,这将允许我们指定数据扩充(对我们的图像进行随机转换,使我们能够不断生成所有都是唯一的图像,而不是在完全相同的图像上进行训练),并使我们能够指定从中加载图像的数据框。

  • 旁注:为什么我们需要增强我们的图像?神经网络训练的方式是一次遍历所有的训练图像(通常是称为“批次”的图像组)。在整个成像集上运行的每个训练被称为一个“时期”一些神经网络将需要多个时期来实际训练到现实世界使用可行的点,但是一次又一次地在相同的图像上训练有过度适应训练数据的风险。我们可以使用ImageDataGenerator来随机改变每个时期的源训练图像。我们可以指定对图像的随机变换(称为“增强”),例如将图像旋转一定的最大度数,水平翻转图像,左右移动图像,或者改变图像的亮度。我们将在我们的脚本中使用这些增强。

让我们建立一个方法,允许我们创建我们需要的所有生成器(例如,训练、验证和测试生成器)。

输入【单元格= 4】

from tensorflow.keras.preprocessing.image import ImageDataGenerator

def create_generators(train, test, size=224, b=32):
  train_generator = ImageDataGenerator(
      rescale=1./255, rotation_range=5, width_shift_range=0.1,
      height_shift_range=0.1, validation_split=0.2
  )
  test_generator = ImageDataGenerator(rescale=1./255)

  baseargs = {
      "x_col": 'paths',
      "y_col": 'labels',
      "class_labels": ['normal', 'pn'],
      "class_mode": 'binary',
      "target_size": (size,size),
      "batch_size": b,
      "seed": 42
  }
  train_generator_flow = train_generator.flow_from_dataframe(
    **baseargs,
    dataframe=train,
    subset='training')
  validation_generator_flow = train_generator.flow_from_dataframe(
    **baseargs,
    dataframe=train,
    subset='validation')
  test_generator_flow = test_generator.flow_from_dataframe(
      **baseargs,
      dataframe=test,
      shuffle=False)

  return train_generator_flow, validation_generator_flow, test_generator_flow

输出

不会有任何输出,因为我们只是在这里定义一个方法。我们将在完成它的功能后调用它。

这里,我们定义了一个名为create_generators的方法,它接受我们的训练和测试集。我们还指定了另外两个参数size=224, b=32。这些是“默认命名参数”(它们的功能与普通参数一样,但只有在方法调用指定的情况下才具有指定的值)。sizeb将用于指示图像应该如何调整大小,以及每个生成器的“批量大小”(即,神经网络在每个时期的每个训练步骤中看到的图像数量)。

接下来,我们创建ImageDataGenerator的两个实例,一个训练图像数据生成器和一个测试图像数据生成器。在训练图像数据生成器中,我们指定了如下一些参数:

  • rescale是一个数字乘以图像中的每个像素。我们将它设为等于1./255,这意味着我们将所有的东西都乘以 1/255。选择这个数字是因为大多数神经网络架构对比例敏感,这意味着网络很难在整个像素值范围(0–255)内学习。相反,我们可以将图像像素重新调整为 0 到 1 之间的值(将整个图像乘以 1/255 即可)。

  • rotation_range是我们拥有的第一个增强参数。因为我们将它设置为 5,我们将随机地顺时针或逆时针旋转每张图片 0 到 5 度。

  • width_shift_rangeheight_shift_range是第二和第三增强参数。它们分别将图像宽度和高度移动图像宽度和高度的 0–10%(0.1)。

  • validation_split是将我们的训练集分成训练集和测试集的参数。因为我们将它的值指定为 0.2,所以我们最初定义为训练集的 20%将被分配给一个验证子集。

对于测试图像数据生成器,我们只需要重新缩放我们的图像(因为我们想在没有任何额外变换的实际图像上进行测试)。

实例化一个ImageDataGenerator对象不足以让数据生成器从我们的数据框中读取图像。为此,我们必须在我们当前拥有的每个数据生成器(一个训练和测试数据生成器)上调用.flow_from_dataframe。现在,让我们深入到前面代码的第二部分:

...
  baseargs = {
      "x_col": 'paths',
      "y_col": 'labels',
      "class_labels": ['normal', 'pn'],
      "class_mode": 'binary',
      "target_size": (size,size),
      "batch_size": b,
      "seed": 42
  }
  train_generator_flow = train_generator.flow_from_dataframe(
    **baseargs,
    dataframe=train,
    subset='training')
...

这里,我们定义了一个有多个键和值的字典。然后这个字典被传递给我们的函数,但是被表示为**baseargs**是做什么的?它实际上将我们的字典扩展为命名参数,这样字典的每个键和值都是一个命名参数的名称和值。所以前面的例子相当于说

train_generator_flow = train_generator.flow_from_dataframe(
    x_col='paths',
    y_col='labels',
    class_labels=['normal', 'pn'],
    class_mode='binary',
    target_size=(size,size),
    batch_size=b,
    dataframe=train,
    subset='training')

使用**语法更方便一点,因为我们将在每个生成器中重复这些参数。

每个参数都执行以下操作:

  • dataframe指定包含图像位置及其相关标签的数据的源数据框。

  • 我们还需要指定哪一列包含图像路径(x_col参数),哪一列包含标签(y_col参数)。

  • 此外,我们需要将类标签(参数class_labels)指定为一个列表(注意:类名在内部被转换为 0 或 1,列表的第一个元素将被视为 0,第二个元素将被视为 1)。

  • 由于我们只处理两个类别来预测,我们可以指定结果(通过class_mode参数)应该被视为一个二元变量(如果我们预测多个事物,我们可以使用categorical作为class_mode)。

  • 我们还可以将图像的大小调整到一个元组中指定的宽度和高度,作为target_size的参数。这里,我们将size设置为 224,这意味着我们将把 224px x 224px 的图像输入到我们的神经网络中(这在后面会变得很重要)。

  • 我们还通过batch_size变量设置批量大小(即,我们一次呈现给神经网络的图像数量)。我们将批处理大小设置为 32,但这可以根据您的需要而定(较大的批处理大小会占用更多内存,较小的批处理大小会占用较少内存,但可能会导致网络学习速度变慢,因为在更新其权重之前,它一次只能看到一个图像)。

  • 此外,对于我们制作的训练和验证生成器,我们可以指定从哪个数据子集进行选择。回想一下,当我们将ImageDataGenerator存储在train_generator变量中时,我们指定了一个 0.2 的验证分割。这将 80%的数据框行分配给定型子集,20%分配给验证子集。因为我们正在制作一个完全“流动”的训练生成器,所以我们想要选择这个训练生成器的训练子集,这可以用subset参数来完成。

  • 最后,我们可以通过指定一个“种子”参数来确保数据分割的可再现性(这只是一个随机数,但是任何从具有该数字的数据帧运行 flow 的人都应该得到与我们相同的图像分割)。

我们将调用了.flow_from_dataframetrain_generator存储在train_generator_flow变量中。

我们还设置了类似于“流动”序列生成器的“流动”验证生成器,除了我们指定subset为原始序列生成器的'validation'子集。

最后,我们设置了与其他两个类似的“流动”测试生成器,除了我们不包括subset参数,并将源数据帧设置为test数据帧。此外,我们禁用数据集中图像的混排。通常,所有的图像在每个时期被混洗,以确保网络不会以相同的顺序暴露于图像;然而,这种行为使得评估图像更加困难(将导致用于评估图像的标签不正确)。

现在,让我们通过调用create_generators方法并显示来自我们的训练生成器的一些图像来看看是否一切正常:

输入【单元格= 5】

train_generator, validation_generator, test_generator = create_generators(train, test, 224, 32)

imgs = train_generator.next()
fig = plt.figure(figsize=(10,10))
for i in range(16):
  fig.add_subplot(4,4,i+1)
  image = imgs[0][i]
  label = 'PNEUMONIA' if imgs[1][i] == 1 else 'NORMAL'
  plt.axis('off')
  plt.imshow(image)
  plt.title(label)

输出

参见图 6-2 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-2

用于查看的肺炎和正常病例的输出网格

我们首先使用traintest数据集以及 224 和 32 分别调用我们的create_generators方法,用于图像大小和批量大小(尽管我们可以省略这两个参数,因为它们在方法定义中有默认值)。

代码的下一部分对来自训练生成器的一批图像进行采样(使用train_generator.next())并创建一个图形。我们将这些图像存储在imgs变量中。train_generator.next()返回两个列表:第一个列表包含所有图像,可以使用imgs[0]访问;第二个列表包含相应的标签,可以使用imgs[1]访问。

接下来,我们将显示该训练批次中的前 16 个图像(注意,该批次中总共有 32 个图像)。

for i in range(16)进行 16 次的 for 循环(而i从 0 开始加 1)。对于 For 循环的每次迭代,我们通过指定三个数字向图像添加一个子情节:前两个数字定义子情节的网格(4x4 以容纳所有 16 个图像),最后一个数字指定图像将绘制在哪个数字子情节中(从 1 开始)。

为了绘制图像,我们将访问存储在imgs[0][i]中的第i个索引训练图像。我们还将从imgs[1][i]获取图像的标签,如果标签等于 1,则将它的值存储为“肺炎”;否则,它会将其存储为“正常”

最后,我们使用plt.imshow方法显示图像(将图像绘制到子情节中),并使用plt.title设置情节标题,传入我们之前设置的标签。

正如您所看到的,一些图像被轻微旋转和移动,所有图像看起来都像我们在测试集中可能看到的可信图像(这是数据扩充的目的)。

现在,我们已经设置了用于神经网络的数据生成器,让我们实际指定神经网络的结构。

你的第一个卷积神经网络:SmallNet

神经网络通常使用以下一般步骤来指定:

  1. 指定网络的结构(即进入网络的所有单个层)。

  2. 编译网络,并指定它如何学习(通过优化器),如何惩罚糟糕的学习(通过损失函数),以及如何衡量其进展(通过指标)。

  3. 指定回调函数,以确定在每个时期结束时是否应该记录任何内容,何时应该保存模型,以及一些参数应该如何更改。

  4. 训练模型。

我们程序的整体结构将如下(注意,这只是一个粗略的草图,而不是实际的代码):

def make_modelname():
    # specify model layers and compile model
    # return compiled model
def get_callbacks():
    # return list of callbacks
def run_model(train_generator):
    model = make_modelname()
    callbacks = get_callbacks()
    # fit the model
    model.fit(train_generator, callbacks)

在这一部分,我们将处理第 1 步和第 2 步。为此,让我们指定一个我们称之为“SmallNet”的神经网络架构,因为它具有相对较少的训练参数。在此之前,我们需要导入几个方法:

输入【单元格= 6】

  • 第一行从 Keras 机器学习库中导入一个称为“顺序”模型的模型类型。它还导入了一个“load_model”方法,我们将在稍后尝试重新训练一个已经保存到磁盘的模型时使用这个方法。

  • 第二行导入了一个名为“Adam”的优化器。Adam 将自适应地改变神经网络的学习速率,以便它调整调整其权重的速度。

  • 第三行导入各种层(将这些层视为神经网络的部分)。我们将在创建“SmallNet”时使用所有这些工具

  • 第四行导入了一些回调方法。如果网络性能停滞,ReduceLROnPlateau将降低学习率,ModelCheckpoint允许我们在每个时期完成后保存网络,TensorBoard允许我们可视化我们网络的训练进度,EarlyStopping允许我们在没有任何改进的情况下提前停止网络的训练(帮助我们避免过度拟合)。

  • 第五行导入了一些指标,以便在网络训练期间进行监控。

  • 第六行导入一个叫做VGG16的神经网络架构。我们将调整这个网络的预训练版本来完成我们的任务。

  • 第七行引入了一个名为plot_model的方法,它让我们能够可视化我们的神经网络结构。

  • 最后一行只是让我们能够获得当前的日期和时间(我们在为培训进度创建日志时将需要它)。

from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Conv2D, MaxPooling2D, Flatten, Dropout
from tensorflow.keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, TensorBoard, EarlyStopping
from tensorflow.keras.metrics import Recall, Precision, AUC
from tensorflow.keras.applications import EfficientNetB4
from tensorflow.keras.utils import plot_model
from datetime import datetime

现在我们已经定义了我们需要的导入,让我们开始指定我们的“smallnet”架构:

输入【单元格= 7】

def make_smallnet():
  SIZE = 224
  model = Sequential()
  model.add(Conv2D(32, (3, 3), activation="relu", input_shape=(SIZE, SIZE, 3)))
  model.add(MaxPooling2D((2, 2)))
  model.add(Conv2D(32, (3, 3), activation="relu"))
  model.add(MaxPooling2D((2, 2)))
  model.add(Conv2D(32, (3, 3), activation="relu"))

  model.add(Flatten())
  model.add(Dense(32, activation="relu"))
  model.add(Dense(1, activation="sigmoid"))

  model.compile(optimizer=Adam(learning_rate=1e-2),
                loss='binary_crossentropy',
                metrics=['accuracy', Recall(name='recall'),
                        Precision(name='precision'), AUC(name='auc')])

  return model

这个方法将被用于在我们的最终训练方法中返回一个“编译”的模型(注意:一个“编译”的模型只是一个准备好被训练的模型)。这种网络体系结构包含按顺序排列的多个层(即每层直接连接到下一层);我们称之为顺序卷积神经网络。这个卷积神经网络(CNN)被分成两个主要部分:卷积部分和密集部分。

在卷积部分(在“Flatten()”行之前),我们创建了三个卷积层和两个最大池层。第一个卷积层由 Keras 神经网络库中的Conv2D方法指定。

让我们进一步分析这一陈述:

Conv2D(32, (3, 3), activation="relu", input_shape=(SIZE, SIZE, 3))

第一个参数32指定了我们想要训练的卷积滤波器的数量。回想一下,这些滤镜从左到右滑过图像,并将滤镜所在的像素乘以滤镜中学习到的任何值。这个过程我们做了 32 次。

过滤器的大小由第二个参数(3,3)指定,这意味着一个 3px x 3px 的正方形将围绕我们的图像移动并进行乘法运算。

activation="relu"参数指定卷积层的激活函数。这基本上是一个应用于来自图像卷积的结果值的函数。在这种情况下,我们使用“relu”激活函数,它将保持正值,并将任何负值设置为 0。我们需要激活函数,因为它允许网络进行梯度下降(即,找出调整权重以最小化损失的最佳方式)。

最后一个参数指定图像的输入形状。在这里,我们将其设置为(SIZE, SIZE, 3)。在方法体中,SIZE是一个我们设置为等于 224 的变量,它对应于我们从训练生成器获得的输入图像的大小。“3”表示我们的图像是 RGB 彩色图像。尽管 x 射线照片只有强度值而没有颜色,但 Kaggle 竞赛的组织者将 x 射线 DICOM 图像保存为. jpg 格式,这将它们转换为 RGB 彩色图像(即使它们在我们眼中看起来是灰色的)。“3”表示每个图像实际上有三个相互堆叠的图像:一个图像包含该图像的“红色”值,另一个包含“绿色”值,最后一个包含“蓝色”值。如果我们有原始强度值,我们会将“3”改为“1”我们将这个Conv2D方法调用包装在一个model.add中,它实际上将这个卷积层(由Conv2D创建)添加到我们正在制作的顺序 CNN 模型中。

  • 边注:当我们实际训练这一层网络时,神经网络需要学习几百个参数,仅仅是这一层。参数的具体数量是 896(可以把这些看作是感知器的权重和偏差,尽管卷积滤波器中没有任何参数)。我们如何计算这个数字?嗯,每个滤镜都是 3x3x3(前两个“3”表示滤镜的宽度和高度;最后 3 个代表我们正在对三通道彩色图像进行操作的事实),其中过滤器的每个单元都是要学习的参数。此外,每个滤波器还必须学习一个通用的“偏差”项。我们有 32 个这样的滤波器要学习,这意味着有 32∫((3∫3∫3)+1)= 896 个参数要学习。如果我们有前一层的输出,我们还需要学习参数来确定它们的权重。卷积模块中要学习的参数的一般公式为

)

我们添加到网络中的第二层是“最大池层”(也称为 maxpool)。所有这一层将做的是缩小我们的图像,在我们的图片中寻找特定的块,只保留最高值的像素。在这种情况下,因为我们将数字(2,2)传递到MaxPool2D层,所以我们将把来自前一卷积层的图像输出分解成小的 2x2 部分(不重叠)。然后,该层将挑选出每个 2x2 部分中最高值的像素。不需要学习任何参数,因为这个操作只是减少输入的算法步骤。

我们将再次按顺序重复卷积➤最大池操作,并跟进最后一个卷积层。在我们的网络中增加更多的卷积可能会产生更好的结果;但是,我们需要确保不要添加太多的最大池层,因为每次应用它们时,图像都会缩小两个。说到这里,我们的图像在这些操作结束时的大小是多少?

在第一次 Conv2D 操作之后,我们的输入 224 x 244 图像将被转换为 222 x 222 x 32 图像。为什么我们两边都丢了两个像素,还加了个“x32”?那么,通过图像的卷积滤波器必须总是通过图像的有效部分(即,我们不能出现滤波器的某些部分在图像上而某些部分不在图像上的情况)。此外,由于我们有 32 个卷积滤波器,我们实际上是为单个输入图像创建 32 个新图像。这就引出了以下关于如何计算卷积中维数变化的通用公式:

)

这里, d 是我们想要为其寻找新维度大小的原始维度的大小(例如,宽度), k 是沿着相同维度的内核的大小, p 是填充参数, s 是步幅参数。公式的输出是 dnew 尺寸大小。让我们试着计算一下我们的新宽度。由于我们的宽度输入尺寸 size 是 224, d 是 224。 k 是我们滤镜的宽度,为 3。Padding ( p ,我们添加到图像边缘的像素数)是 0(这是默认值,除非另有说明)。Stride ( s ,过滤器在每一步上移动的量)仅为 1。插上,[(224-3+2*0)/1)]+1 = 222。因为我们的输入高度与输入宽度相同,并且过滤器高度与过滤器宽度相同,所以我们的新高度是 222。x32 来自于这样一个事实,即有 32 个过滤器,每个创建一个新的图像。

在最大池操作之后,我们的维度将减少一半。在我们的原始维度是奇数的情况下,我们向下舍入。因此,这意味着我们在最大池层之后的新维度的宽度和高度将是 111 (222/2 ),通道是 32(也就是从卷积中产生的图像数量)。

下一个卷积层与第一个相同,但这次我们将通道数量从 32 个增加到 64 个。通过数学运算,新的宽度和高度尺寸将为[(111-3+2*0)/1]+1 = 109。新的通道数将为 64,最终尺寸为 111 x 111 x 64。然后,我们接着使用一个最大池,这将维度降低到 111/2 = 54.5(向下舍入)= 54(最终大小= 54 x 54 x 64)。接下来,我们进行另一个卷积运算,保持通道数不变,因此最终尺寸为 52 x 52 x 64。正如您所看到的,每个 maxpool 操作都可以减小我们的图像大小,我们不能低于 1x1 图像(还要注意,如果步幅> 1,卷积可以缩小图像)。

第二次卷积需要学习的参数总数是 18496(64∫((3∫3∫32)+1),第三次卷积需要学习的参数总数是 36928(64∫((3∫3∫64)+1)。如您所见,我们已经有超过 56,000 个参数需要学习,而且我们甚至还没有完成网络的指定。

下一个代码块将获取我们拥有的卷积图像(一个尺寸为 52 x 52 的 64 通道图像),并将其“展平”,以便它们现在可以表示单个的感知器。事实上,我们总共创造了 173,056 个($ = 52 52 64$)感知机。这是通过调用model.add(Flatten())完成的。

接下来,我们将 173,056 个感知器密集地连接到 64 个感知器(通过指定model.add(Dense(64...)))。这将 173,056 个感知器中的每一个连接到 64 个感知器中的每一个,形成总共 173065*64 个连接(这些是我们需要学习的参数)。此外,我们需要学习 64 个感知器中每个感知器的偏差,这样这一层要学习的参数总数就达到了 11075648。我们还在这一层设置了一个 relu 激活函数(将负输出设置为 0,不改变正输出)。这让我们看到了这个密集层的最后一行代码model.add(Dense(64, activation="relu"))。注意,前面提到的所有层,Conv2DMaxPool2DFlattenDense,都来自于tensorflow.keras.layers子模块(它包含许多许多其他层供您检查)。

最后,我们有了网络的最后一层。这是唯一一个实际上被约束为特定值的图层,因为该图层的输出将用于评估预测。我们将建立一个密集层,但只有一个输出感知器。这意味着来自前一层的所有 64 个感知器将被连接到这一个输出层(并且我们需要为这一层学习 64 个权重+ 1 个偏置参数= 65 个参数)。我们还将在这一层设置一个“sigmoid”激活函数(沿着“S”曲线从 0 到 1 约束输出;参见图 6-3 比较 sigmoid 和 relu 功能)。这些激活函数的图形可以在图 6-3 中找到。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-3

Sigmoid 和 ReLU 激活函数图

  • 边注:把一个神经元的预激活功能输出看作是馈入这些功能的“x”值。在 sigmoid 函数中,无论我们放入什么样的 x 值,y 值都会一直在 0 到 1 的范围内。在 relu 函数中,如果我们输入一个正的 x 值,我们会得到一个正的 y 值。如果 x 为负,我们得到 0。还有许多其他的激活函数,如身份,二进制步骤,逻辑,tanh,arctan,leaky relu,softplus 等等。

但是,为什么我们选择我们的输出只是一个单一的感知器,它有一个 sigmoid 函数呢?我们的分类任务是二元分类,这意味着只有两种可能的输出。真的,这可以用 0 到 1 范围内的单个值来表示;因此,我们只需要一个输出来表示它。如果输出值低于某个阈值(比如 0.5),我们将认为输出是正常的;否则,就是肺炎了。完美的预测将产生 1 或 0 的输出值(肺炎或正常);然而,这种情况很少发生。但是来自感知器的原始输出值也可以被认为是某样东西被认为是肺炎病例的概率(因为输出被限制在 0 和 1 之间),这对于给出图像是肺炎的“置信度”是有用的。如果您将此网络用于其他任务,如果类别数大于 2,您应该将密集输出的数量设置为您拥有的不同类别数。

最后一段代码由模型编译语句组成。该语句包含关于什么是模型的损失函数(loss='binary_crossentropy')、什么是用于规定其学习速率的优化器(optimizer='adam')以及在每个训练步骤上应该报告什么度量的信息(列表从metrics=['accuracy', ...]开始)。

损失函数被设置为binary_crossentropy。回想一下,损失函数用于量化神经网络的错误程度,更重要的是,它是确定神经网络如何在反向传播过程中调整其权重和偏差的关键组成部分。二元交叉熵公式如下:

)

这看起来很复杂,但是它实际上是做什么的呢?假设神经网络最后一个感知器对某个样本输出 0.73。这个样本实际上是一个肺炎样本,所以如果我们的网络是最好的,它应该输出 1。显然,网络需要改进才能达到这一点,因此,应该告诉它有多“错误”。我们可以用二元交叉熵公式来计算。暂且忽略求和项,设p(yI)等于 0.73,设 y i 等于 1。另外,我们将使用基数为 2 的对数,而不是基数为 10 的对数。损失将会是)。请注意,我们需要翻转符号,因为损耗通常是指应该最小化的正数,所以最终损耗是 0.31。让我们试试另一个例子。假设最后一个感知器输出的值为 0.73,但这是一个真正正常的图像。这真的很糟糕,因为在这种情况下,网络应该输出一个“0”。让我们看看损失会是什么:)。翻转符号,我们得到 1.309 的最终损失。

我们刚刚看到,与距离非常远的预测相比,距离非常近的预测产生的损失更低。这正是我们想要的行为,因为我们的神经网络旨在始终降低每批损失函数的输出。对于一批中的所有图像(由)项处理),我们简单地将它们的损失相加,取其平均值,并乘以-1(由)处理)。如果你要预测多个类别,你可以使用一个“类别交叉熵”损失函数,它与前面描述的损失函数非常相似。对于回归任务(例如,从 x 光片预测脊柱弯曲度),您可以使用均方误差损失(预测值和实际值的平方之差)。

现在让我们继续讨论优化器。我们使用的优化器叫做“Adam ”,是自适应矩估计的缩写。对于我们模型中的每个参数,它将基于参数是否与频繁特征(在这种情况下,它将保持低学习率)或不频繁特征(在这种情况下,它将保持高学习率)相关联来改变学习率(即,与参数相关联的权重或偏差将改变多少)。“Adam”优化器的工作原理以及它与其他优化器(如随机梯度下降、adagrad、rmsprop 等)的不同之处还有很多细微差别。;然而,这不值得深究(但如果你想看的话,这里的就是那些背后的所有数学)。一般来说,它被广泛使用,更重要的是,它在 Keras 库中很容易获得,所以我们只需要在编译模型时指定一行代码optimizer='adam'

最后,我们指定我们希望在每个训练步骤中跟踪的度量,然后从方法中返回模型。这些度量在 compile 方法的 list 参数中指定,如下所示:“准确性”(Keras 仅通过字符串本身即可识别)、召回率、精确度和 AUC。这些名称是不言而喻的,将只打印出模型中每个训练步骤的准确度、召回率、精确度和曲线下面积(对于 ROC 曲线)。对于最后三个指标,我们可以指定一个“name”参数,这个参数决定了它们在训练过程中如何显示出来。还要注意,最后三个指标都来自一个叫做tensorflow.keras.metrics的 Keras 子模块。

现在我们已经建立了 smallnet,离训练网络只有一步之遥了。在我们这样做之前,我们需要设置一些回调,这将允许我们监控网络的进展,保存其最佳训练周期,如果它没有改善就停止它,并将其踢出任何学习率停滞期。

回调:张量板、提前停止、模型检查点和降低学习率

我们将定义一个名为get_callbacks的方法,该方法返回一个回调列表,供以后在训练过程中使用。让我们看看这个方法是什么样子的:

输入【单元格= 8】

def get_callbacks(model_name):

  logdir = (
      f'logs/scalars/{model_name}_{datetime.now().strftime("%m%d%Y-%H%M%S")}'
  )
  tb = TensorBoard(log_dir=logdir)
  es = EarlyStopping(
        monitor="val_loss",
        min_delta=1,
        patience=20,
        verbose=2,
        mode="min",
        restore_best_weights=True,
    )
  mc = ModelCheckpoint(f'model_{model_name}.hdf5',
                      save_best_only=True,
                      verbose=0,
                      monitor='val_loss',
                      mode='min')

  rlr = ReduceLROnPlateau(monitor='val_loss',
                          factor=0.3,
                          patience=3,
                          min_lr=0.000001,
                          verbose=1)
  return [tb, es, mc, rlr]

get_callbacks方法只接受一个名为model_name的参数,这只是模型的名称,它将帮助我们保存一些与模型相关的文件(以确保它不会覆盖我们文件系统中存在的任何其他内容)。

我们要用的第一个回调是 TensorBoard 回调。TensorBoard 是一个可视化工具,我们稍后会看到。但它基本上允许我们通过给我们监控我们定义的各种度量标准的能力来查看训练过程进行得如何。图 6-4 展示了 TensorBoard 的样子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-4

张量板可视化

它可以在您开始训练过程之前或之后加载,方法是运行下面的单元(但在我们完成所有训练之前,我们不会这样做):

%load_ext tensorboard
%tensorboard --logdir logs

如果你注意到,在右上角,我们正在查看标量面板,它是我们拥有的所有不同指标的集合。TensorBoard 将自动读取目录(由--logdir FOLDERNAME指定,在本例中为logs)并尝试查找 TensorBoard 日志文件。只有在设置神经网络进行训练时将 TensorBoard 回调传递给神经网络,才会生成那些 TensorBoard 日志文件;否则,它就没有什么可情节的了。这些日志文件将包含训练和验证过程中每个时间点的所有指标(也称为标量)。TensorBoard 将绘制这些曲线供您查看(这有助于确定您的网络是否仍在改进)。

我们需要为 TensorBoard 回调指定的只是保存日志的目录的文件路径。由于我们可能会多次运行我们的网络,我们希望确保以前的运行不会被覆盖。我们可以通过在文件夹路径中包含网络运行的日期和时间来做到这一点。datetime库提供了一个名为datetime的子模块,上面有一个.now()方法,允许我们以 Python 对象的形式获取当前时间。我们可以通过调用datetime.now()上的.strftime()并传入下面的字符串"%m%d%Y-%H%M%S"来将其转换为一个字符串,该字符串是打印月(%m)、日(%d)和年(%Y)的简写,后跟一个“-”符号,然后是当前小时(%H,24 小时制)、分钟(%M)和秒钟(%S)。如果我在 7 月 26 日下午 12:24 又 36 秒运行这个程序,得到的字符串将是 07272021-122436。我们将这个值插入到通用字符串"logs/scalars/{model_name}_{the date + time}"中。

第二次回调是 EarlyStopping 回调。顾名思义,它将提前停止网络的训练(这意味着如果我们将网络设置为训练 100 个历元,那么如果满足某些条件,这个回调可能会在某个历元数结束时停止其训练)。我们为什么要这么做?对于我们的模型来说,防止过度拟合是非常有用的。如果我们监控验证损失,并且在 20 个时期内没有看到它提高 1%,这可能意味着网络已经完成了它所能学习的一切。monitor="val_loss"告诉回调密切关注验证损失。min_delta=1告诉回调,看看在patience=20时期之后,它是否提高了 1%(按照mode='min'指示的向下方向)。verbose=2用于在停止网络时提前打印出来。最后,我们将模型设置回最佳点(由restore_best_weights=True指定)。

第三个回调是ModelCheckpoint回调。这个回调将在每个时期将模型的一个版本保存到 Colab 的文件系统中(有一些警告)。第一个参数是模型文件名应该是什么(在这种情况下应该是'model_smallnet.hdf5',因为model_name将是“smallnet”)(请继续关注我们在哪里传递这个参数)。然而,我们还指定了一个save_best_only=True参数,这意味着我们将只在每个时期结束时保存模型,如果它击败了先前保存的模型。我们如何确定哪个模型胜出?我们查看验证损失(在参数monitor='val_loss'中指定),并将选择较低的那个(mode='min')。完成模型训练后,我们可以下载 model_smallnet.hdf5 文件,并将其重新上传到 Colab,或者在我们的本地机器上运行它。

最后一个回调是ReduceLROnPlateau回调。一般来说,学习率太高会导致网络无法收敛。在大多数情况下,Adam 优化器应该能够自己调整学习速率,但有时,它需要强制降低基线学习速率,以继续进一步优化并让网络继续学习。如果验证损失在 3 个时期(monitor='val_loss'patience=3)内没有变化,它将学习率降低 0.3 倍(factor=0.3),并且将继续这样做,直到学习率为 0.000001 ( min_lr=0.000001)。当它降低学习率时也会打印(verbose=1)。

现在我们已经定义了所有的回调,让我们开始定义培训是如何进行的!

定义拟合方法并拟合 Smallnet

我们将创建一个调用 make smallnet 方法和 get callbacks 方法的方法,同时训练网络。我们还将看到如何获得模型的摘要(即,它所拥有的所有层)以及所有模型层如何相互反馈的图表。这是该方法的定义。注意:我们稍后将回到这个方法,所以您需要在几个部分中编辑它(我通过在单元格编号旁边标注一个 WIP 标签来表示它是一个正在进行的工作)。

输入[CELL=9][WIP v1]

def fit_model(train_generator, validation_generator, model_name,
              batch_size=32, epochs=15):

  if model_name == 'smallnet':
    model = make_smallnet()

  model.summary()

  plot_model(model, to_file=model_name + '.jpg', show_shapes=True)

  model_history = model.fit(train_generator,
                            validation_data=validation_generator,
                            steps_per_epoch=train_generator.n/batch_size,
                            validation_steps=validation_generator.n/batch_size,
                            epochs=epochs,
                            verbose=1,
                            callbacks=get_callbacks(model_name))
  return model, model_history

这个方法所做的事情如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-5

SmallNet 架构输出

  1. 它接受我们之前制作的训练和验证生成器。这些将在模型拟合过程中使用(数据将在训练生成器图像上进行训练;它将在验证生成器图像上的每个时期被评估)。

  2. 它检查我们传入的model_name是否等于'smallnet'。如果是,我们将调用make_smallnet()方法,该方法将编译后的 Keras 模型返回给我们(我们将把它存储在model中)。

  3. 然后,我们将打印出一个模型摘要(使用model.summary()),其中包含关于层数、每层大小以及要训练的参数数量的信息。

  4. 然后我们将调用一个方法plot_model(在tensorflow.keras.utils子模块中定义)并传入我们的model、我们想要保存plot_model结果的文件名(只是在模型名后面加上一个.jpg来表示它是一个图像文件)和show_shapes=True参数(这使得图表更有趣)。一旦我们调用它,这将产生如图 6-5 所示的模型(这将在 Colab 文件目录中;没看到就刷新一下)。

请注意它是如何包含我们指定的所有层的:三个卷积层、两个 max pooling 层、flatten 层和两个密集层。它还有一个由 Keras 添加的输入层,用于表示图像输入。

最后,我们调用model.fit,它实际上训练了我们的模型。它接受训练生成器和验证生成器(validation_data=validation_generator),还需要关于训练和验证所需步骤数量的信息。一个步骤被认为是通过一批图像的一次运行,因此所有的步骤将是批的数量。我们可以通过调用generator_name.n(获取图像总数)并用它除以batch_size(方法声明中的默认命名参数)来得到。steps_per_epoch等于训练生成器中的批次数量,validations_steps等于验证生成器中的批次数量。我们将verbose=1设置为获取训练的进度记录,并将回调设置为get_callbacks()方法调用的结果(它只返回我们想要跟踪的回调列表)。

model.fit调用返回模型的历史(这就是在每个时期训练和验证集的度量)。在方法的最后,我们返回训练好的模型(在model中)和它的历史model_history

现在我们已经指定了训练 SmallNet 网络的方法,让我们实际训练它。注意:运行下一个单元大约需要 30 分钟。

输入【单元格= 10】

small_model, small_model_hist = fit_model(train_generator,
                                          validation_generator,
                                          'smallnet', epochs=15)

输出

参见图 6-6 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6

模型训练期间的输出

在这个输出中,我们可以看到一些有趣的事情。首先,在第一部分(在“纪元”之前),我们得到模型摘要(从model.summary()生成)。看起来我们之前对每层的参数数量和维度的计算是正确的!这个模型总结有助于确定您是否在正确的轨道上,并且正确地指定了您的模型。

看到这个之后,你会看到训练过程开始了。我们指定我们应该为 15 个纪元训练我们的网络。在每个时期内,有 117 个步骤,代表我们拥有的 117 个训练批次。在每个步骤之后,您将看到“丢失”和其他度量更新:这些是网络在看到每个步骤中的所有图像(即,一批)之后报告的训练度量。在每个时期(即,运行所有 117 个训练批次),您会看到打印出来的loss将开始随时间减少。在这里,我们看到它从 0.56 到 0.25,再到 0.1659。我们还看到训练集的准确率(打印文本中的accuracy)从 77.69%上升到 93.28%。您还可以看到我们指定的所有其他指标:召回率、精确度和 AUC。如果在 epoch 完成训练后向右滚动输出,您还会在验证集上看到那些相同的指标(这些指标前面有val_字符串)。如果您看到这些验证指标与训练指标相差甚远,这表明您的网络负荷过重。

您还会在前面的输出中看到,在 epoch 5,由于触发了 ReduceLRonPlateau 回调条件,我们将学习率降低到了 0.0003。这是因为验证损失val_loss处于平稳状态(从 0.43 到 0.20 到 0.24 到 0.21 到 0.25),而不是持续下降。这在第 14 纪元对我来说又发生了,但对你来说可能不会发生。学习率下降后,val_loss又开始下降,我还看到val_accuracy快速上升。

我们甚至可以通过以下方式启动 TensorBoard 来查看培训课程:

输入【单元格=可选但在 10 之后】

%load_ext tensorboard
%tensorboard --logdir logs

在培训课程结束时,指标如下:

  • 验证精度 : 0.9402

  • 验证召回 : 0.9608

  • 验证精度 : 0.9580

  • 验证 AUC : 0.9794

这些是我们自己定义的网络上的优秀统计数据!在我们开始评估这个模型的性能之前,让我们继续定义另一个可能击败我们 94%准确率的模型。

你的第二个卷积神经网络:用 VGG16 进行迁移学习

SmallNet 非常适合这项任务;然而,除了我们刚刚制作的相对简单的玩具之外,还有其他经过尝试和测试的神经网络架构。其中一种架构称为 VGG16。诚然,以深度学习的标准来看,它有点老了(它是在 2014 年制作的);然而,它在 ImageNet 分类挑战中击败了竞争对手(了解如何将 1400 万张图像分类到 1000 个类别)。VGG16 包含 13 个卷积层和 3 个密集层,总计超过 1400 万个参数用于训练。由于它在 ImageNet 竞赛中表现如此出色,很可能权重和偏见“学会”了如何提取突出的图像特征,这些特征可归纳到最初训练的 1000 个不同的类别中。因此,对我们来说,以某种方式利用现有的权重和偏差来使我们的分类任务更好可能是有用的,因为 VGG16 似乎在识别图像方面做得非常好。

然而,问题是我们不能直接使用 VGG16。不幸的是,ImageNet challenge 不包含任何使用 x 射线的训练图像,而且肯定没有肺炎与正常的课程。但是,也许有某种方法可以保留网络的主要卷积部分,只删除网络的最后一层,这一层(正如我们在 smallnet 讨论中提到的)是我们获取分类值的地方。正如 SmallNet 对每个类都有一个最后层的感知器(在本例中只有 1 个),VGG16 也有一组最后层的感知器(正好 1000 个)用于对 ImageNet 图像进行分类。如果我们只是从在 ImageNet challenge 上训练的 VGG16 模型中取出最后一个具有 1000 个感知器的密集层,我们可以利用网络内部的卷积权重(即,真正擅长提取图像特征进行学习的权重)。将先前训练的网络中的权重用于新目的的过程被称为“迁移学习”

  • 边注:另一种可能是采用 VGG16 架构,从头开始训练。不幸的是,它需要很长时间才能收敛到准确预测数据的点,并且更适合在更大的数据集上训练。正如我们将很快演示的那样,实际上根据预先存在的权重训练网络就足以获得非常好的准确度。

让我们看看如何做到这一点。在定义了make_smallnet()方法的单元格(单元格 7)后,插入以下make_VGGnet方法:

输入【单元格=单元格#7 后的新单元格】

def make_VGGnet():
  SIZE = 224
  m = VGG16(weights = 'imagenet',
                include_top = False,
                input_shape = (SIZE, SIZE, 3))
  for layer in m.layers:
    layer.trainable = False

  x = Flatten()(m.output)
  x = Dense(4096, activation="relu")(x)
  x = Dense(1072, activation="relu")(x)
  x = Dropout(0.2)(x)
  predictions = Dense(1, activation="sigmoid")(x)

  model = Model(inputs=m.input, outputs=predictions)
  ## Compile and run

  adam = Adam(learning_rate=0.001)
  model.compile(optimizer=adam,
                loss='binary_crossentropy',
                metrics=['accuracy', Recall(name='recall'),
                        Precision(name='precision'), AUC(name='auc')])
return model

一行一行地走

  • 我们在变量SIZE中定义将要接受的输入图像的大小,该变量将被传递给其他方法。我们将其保持在 224,因为这是 VGG16 模型的默认输入大小。

  • 然后我们通过调用VGG16实例化一个新的 VGG16 模型对象。我们指定我们想要使用来自 ImageNet 数据集的权重(weights='imagenet'),我们不想包含用于进行 1000 个分类的网络的最后一层(include_top=False),并且我们想要输入 224 x 224 x 3 个图像(input_shape = (SIZE, SIZE, 3),注意 3 代表图像具有 3 个颜色通道的事实)。我们将这个网络实例存储在变量m中。

  • 然后我们遍历网络中的所有层(for layer in m.layers),并将每层上的trainable属性设置为False。这将阻止在 ImageNet 竞赛中学习的权重被改变(也称为“冻结”权重),这正是我们想要的,因为我们需要保留 VGG16 提取图像特征的能力,就像它在 ImageNet 数据集所做的那样。

  • 然后,我们定义一些附加层添加到网络中。注意,我们做 smallnet 的时候是用model.add做的。在这种情况下,我想向您展示一种不同的方式,您可能会在网上更经常看到:

  • 首先,我们将通过调用Flatten() (m.output)来展平存储在m中的修改后的 VGG16 模型的输出层。这种语法可能看起来很奇怪,但 flatten 所做的实际上是返回一个接受单个输入的方法,即前一层的输出,以将该 Flatten 层附加到该层。我们将更新后的模型分配给变量x

  • 然后,我们将创建一个具有 4096 个感知器的密集连接层,该层附有一个 relu 激活函数,传入x以继续将该层添加到模型中(Dense(4096, activation="relu")(x))。我们将把这个值赋回给x

  • 同样,我们将制作另一个有 1072 个感知器的密集连接层。

  • 我们将添加一个“Dropout”层,它将在训练过程的每一步随机地从上一层到下一层随机地清除 20% ( Dropout(0.2))的输入权重。这有助于防止过拟合,因为每次训练步骤完成时,训练过程的某些部分会忽略一些权重(因为它们的权重被丢弃层设置为 0)。

  • 然后,我们将添加一个单一的感知器密集层,类似于我们对 SmallNet 的最后一层。我们把这个赋值给变量predictions

  • 最后(在编译模型之前),我们需要通过用输入大小(inputs=m.input)和输出(包含所有先前的层)实例化一个普通的Model对象来告诉 Keras 这是一个正常的模型。目前,我们将层存储在predictions中,所以我们将它传递给outputs=命名参数。

为了编译我们的网络,我们需要使用 Adam 优化器的非默认版本(只是向您展示如何在优化器上指定不一定是默认的参数)。我们使用来自tensorflow.keras.optimizersAdam优化器,并将其初始学习率设置为 0.001,得到最终的线adam = Adam(learning_rate=0.001)。我们向 model.compile 方法传递与 SmallNet 相同的损失和度量,并从该方法返回模型。

为了运行这个方法,我们需要稍微改变一下我们的fit_model方法,并且,当我们这样做的时候,让我们也给自己一个能力来获取我们生成的模型的训练版本,并且继续训练它:

输入【单元格=9】【最终】

def fit_model(train_generator, validation_generator, model_name,
              batch_size=32, epochs=15, model_fname=None):
  if model_fname == None:
    if model_name == 'smallnet':
      model = make_smallnet()
    if model_name == 'vgg':
      model = make_VGGnet()
    model.summary()
    plot_model(model, to_file=model_name + '.jpg', show_shapes=True)
  else:
    model = load_model(model_fname)

  ...# REST IS THE SAME!
  model_history = model.fit(train_generator,
                            validation_data=validation_generator,
                            steps_per_epoch=train_generator.n/batch_size,
                            validation_steps=validation_generator.n/batch_size,
                            epochs=epochs,
                            verbose=1,
                            callbacks=get_callbacks(model_name))
  return model, model_history

首先,我们指定了一个名为model_fname的新方法参数。此参数将接受到定型模型的字符串文件路径。如果没有指定,它的缺省值是 none,我们在方法的开始进入if else分支的第一部分。在这里,我们添加了另一个if语句来检查model_name是否是‘vgg’(如果是,我们调用刚刚创建的 make VGG 方法)。

如果我们碰巧有一个我们已经训练过的模型,我们想进一步训练它,我们可以通过给model_fname指定一个参数来实现。回想一下,每当我们的ModelCheckpoint回调触发时(即,每当我们有一个“最佳”时期时),我们生成一个文件名为model_INSERTMODELNAME.hdf5的预训练模型。如果您检查您的 Colab 笔记本文件窗格,您应该看到您有一个model_smallnet.hdf5

现在我们实际上已经设置了我们的 vgg 网络,从我们刚刚编辑的fit_model方法调用,让我们调用它

输入【单元格= 11】

vgg_model, vgg_model_hist = fit_model(train_generator,
                                      validation_generator,
                                      'vgg', epochs=15)

输出

Model: "model_2"
_______________________________________________________________
Layer (type)                 Output Shape              Param #
===============================================================
input_6 (InputLayer)         [(None, 224, 224, 3)]     0
_______________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792
_______________________________________________________________
block1_conv2 (Conv2D)        (None, 224, 224, 64)      36928
_______________________________________________________________
block1_pool (MaxPooling2D)   (None, 112, 112, 64)      0

...LOTS OF OTHER LAYERS THAT ARE OMITTED....
===============================================================
Total params: 121,872,289
Trainable params: 107,157,601
Non-trainable params: 14,714,688
_______________________________________________________________

...EPOCHS ALSO OMITTED

哇,我们有超过 1.07 亿个参数要训练。不可训练参数是我们在每个原始 VGG16 层上调用layer.trainable = False时冻结的权重。剩下的 107,157,601 个参数是我们在密集层中定义的(几个密集层很快就增加了几百万个参数!).

如果我们观察训练进度,我们会发现它往往进行得更顺利一些(只触发 ReduceLRonPlateau 回调一次),而且我们通常会看到更高的精度和 AUC,这很好!我得到了 96.58%的最终验证准确率,比 93.28%有所提高。

在深入研究评估指标之前,让我们比较一下这两个网络的培训过程。

如果您不能使用 TensorBoard(它有时会出错),您可以使用以下方法绘制出两个网络的训练与验证准确性和损失的历史记录:

输入【单元格= 12】

def plot_history(history):

    fig = plt.figure(figsize = (18 , 6))

    fig.add_subplot(1,2,1)
    plt.plot(history.history['loss'])
    plt.plot(history.history['val_loss'])
    plt.title('model loss')
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.legend(['train loss', 'valid loss'])
    plt.grid(True)
    plt.plot()

    fig.add_subplot(1,2,2)
    plt.plot(history.history['accuracy'])
    plt.plot(history.history['val_accuracy'])
    plt.title('model accuracy')
    plt.xlabel('epoch')
    plt.ylabel('accuracy')
    plt.legend(['train acc', 'valid acc'])
    plt.grid(True)
    plt.plot()

我们在这里所做的就是定义一个名为 plot history 的方法,该方法接受一个模型训练历史,它是我们从model.fitfit_model()返回的值之一。我们创建一个图形,并添加一个支线剧情(将有两个图像,安排在一行两列)。第一幅图像将绘制history.history['loss']history.history['val_loss']阵列,它们是每个时期结束时的训练和验证损失。我们将该图命名为“模型损失”,并将 x 轴标签设置为“时期”,将 y 轴标签设置为“损失”我们还给出了每条线的图例:第一条线将被称为“列车损失”,第二条线将被称为“有效损失”(matplotlib 根据用plt.plot()绘制线的顺序知道哪个图例项与哪条线匹配)。最后,我们选择显示一个网格并绘制数据。我们对另一个图重复相同的过程,但这样做是为了训练准确性和验证准确性。我们来看看调用后的结果是什么:

输入【单元格= 13】

plot_history(small_model_hist)

输出

参见图 6-7 了解 SmallNet 的模型历史图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7

SmallNet 模型历史

输入【单元格= 14】

plot_history(vgg_model_hist)

输出

参见图 6-8 获取 VGG16 的模型历史图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-8

VGG16 车型培训历史

我们可以看到,VGG 网络实际上似乎在纪元 11 前后经历了一段困难时期。这可能与学习率的下降相一致。然而,我们可以看到验证和准确性曲线彼此相对紧密地跟随,这表明到目前为止我们还没有过度拟合。

现在我们已经查看了训练曲线,让我们看看网络在使用 Grad-CAM 技术评估图像时实际关注的是什么。

使用 Grad-CAM 可视化输出

Grad-CAM 是一种算法,允许我们生成热图,显示图像的哪些部分对网络的最终分类决策贡献最大。基于 Grad-CAM 图像并观察它们如何工作,可以获得大量的直觉。事不宜迟,我们开始吧。

我们将使用一个名为 VizGradCam 的库。不幸的是,它在 pip 包管理系统上不可用,所以我们需要直接从作者的 GitHub 页面下载(这是一个托管任何人都可以查看的代码的网站,也称为开放源代码)。我们将使用下面的块来实现这一点:

输入【单元格= 13】

!git clone https://github.com/gkeechin/vizgradcam.git
!cp vizgradcam/gradcam.py gradcam.py

输出

Cloning into 'vizgradcam'...
remote: Enumerating objects: 64, done.
remote: Counting objects: 100% (64/64), done.
remote: Compressing objects: 100% (57/57), done.
remote: Total 64 (delta 30), reused 24 (delta 7), pack-reused 0
Unpacking objects: 100% (64/64), done.

如果你看到了,你就万事俱备了。我们已经下载了 VizGradCam 代码,取出了包含我们需要的功能的文件,并将其复制到我们的主目录中。

接下来,我们将定义一个方法,允许我们获得 Grad-CAM 输出,并打印出我们的预测和它们的置信度。

输入【单元格= 14】

from gradcam import VizGradCAM

def display_map_and_conf(model, test_generator):
  imgs = test_generator.next()
  fig = plt.figure(figsize=(15,5))

  for i in range(3):
    fig.add_subplot(1,3,i+1)
    image = imgs[0][i]
    label = 'PNEUMONIA' if imgs[1][i] == 1 else 'NORMAL'
    VizGradCAM(model, image, plot_results=True, interpolant=0.5)
    out_prob = model.predict(image.reshape(1,224,224,3))[0][0]
    title = f"Prediction: {'PNEUMONIA' if out_prob > 0.5 else 'NORMAL'}\n"
    title += f"Prob(Pneumonia): {out_prob}\n"
    title += f"True Label: {label}\n"
    plt.title(title)

这个方法将接受一个经过训练的模型和一个测试生成器(我们在前面已经定义了)。它将从测试生成器(test_generator.next())中抓取一批图像,然后创建一个图形供我们绘制 Grad-CAM 结果。

对于我们的图像批次(存储在imgs中)中的前三个图像(for i in range(3)),我们将为我们的图形添加一个子图(从 1 开始计数),然后从我们的批次中抓取图像(imgs[0][i]包含该批次的第 i 个图像)和相关联的标签(包含在imgs[1][i]中);如果标签== 1 或“正常”,我们也将标签重新编码为“肺炎”,否则有助于解释)。

接下来,我们从库中调用 VizGradCam 方法(我们使用from gradcam import VizGradCAM将其导入到文件的顶部),传入我们训练的模型、我们想要可视化的图像以及两个命名的参数plot_results(将结果绘制到 matplotlib)和interpolant(确定覆盖的相对亮度)。

接下来,我们得到图像是肺炎的概率的预测。我们通过使用名为model.predict的方法并传入图像来实现这一点。然而,我们的模型通常期望图像是批处理的形式,所以我们需要将图像整形(通过调用image上的.reshape)成 1 x 224 x 224 x 3 的图像(“1”指的是批处理大小)。预测调用的结果是嵌套在一个数组中的一个数组的概率,我们可以通过调用model.predict(...)[0][0]得到它(其中第一个[0]将我们带到第一个数组,第二个[0]将我们带到嵌套的数组)。

接下来,我们将通过对三个单独的字符串进行字符串插值来制作图表的标题。我们通过给变量 title 加上一个title +=来将每个字符串相互追加,这相当于做title = title + ...(还有一个小的旁注:\n是一个特殊的字符,用来换行)。对于我们的预测类,如果某个事物的概率为> 0.5,我们将其定义为肺炎;否则就不是肺炎了。

我们将该方法称为:

输入【单元格= 15】

display_map_and_conf(vgg_model, test_generator)

输出

VGG16 的 Grad-CAM 结果参见图 6-9 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-9

Grad-CAM 结果 VGG16

输入【单元格= 16】

display_map_and_conf(small_model, test_generator)

输出

SmallNet 的 Grad-CAM 结果参见图 6-10 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-10

Grad-CAM 结果小型网络

红色区域是网络关注较多的区域,蓝色区域是网络关注较少的区域。总的来说,我们可以看到 vgg 网络倾向于聚焦于解剖结构,例如第一幅图像中的胸膜。然而,它也集中在第二个图像中的心脏。在最后一张图中,它聚焦于肺部的不同肺叶,但也有边缘的随机部分。smallnet 似乎将注意力集中在第一幅图像的边缘区域,第二幅图像左上角的标签上写着“A-P ”,第三幅图像的肋骨。

总的来说,VGG 似乎在关注更符合解剖学的结构;然而,在这些热图能够被用来实际帮助放射科医师建议他们应该关注的领域之前,似乎需要更多的培训。然而,特别值得关注的是,SmallNet 聚焦于图像中的 A-P 标签,这可能表明 x 射线图像中出现的字母标签的某些内容可能在提示网络确定分类,而不是解剖结构本身。看到这些差异就是为什么运行 Grad-CAM 很重要!现在开始网络评估。

评估 SmallNet 与 VGG16 的性能

当我们在上一章中制作基本决策树分类器时,我们能够使用 scikit-learn 获得一些关于模型性能的统计数据,如精度、召回率、准确度、AUC 和 ROC 曲线。

由于我们需要获得两个网络的这些统计数据,我们将只创建一个方法来自动打印这些统计数据并显示相关的图表:

输入【单元格= 17】

from sklearn.metrics import (roc_curve, auc, classification_report,
RocCurveDisplay, confusion_matrix, ConfusionMatrixDisplay)

def get_stats(model, generator, model_name):
  preds = model.predict(generator)
  pred_classes = [1 if x >= 0.5 else 0 for x in preds]
  true_vals = generator.classes
  print("CLASSIFICATION REPORT")
  print(classification_report(true_vals, pred_classes, target_names=['normal','pn']))
  cm = confusion_matrix(true_vals, pred_classes)
  disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['normal','pn'])
  disp.plot(values_format='')
  fpr, tpr, thresholds = roc_curve(true_vals, preds)
  auc_val = auc(fpr, tpr)
  print("AUC is ", auc_val)
  display = RocCurveDisplay(fpr, tpr, auc_val, model_name)
  display.plot()
  plt.show()

这个名为get_stats的方法接收经过训练的模型、一个运行这些统计数据的生成器(测试生成器),以及将用于绘制 ROC 曲线的模型名称。

该方法的第一行在传入的生成器(这将是我们的测试生成器)上获得模型的预测。这些预测值的范围是 0–1。对于我们的一些统计数据,我们需要将这些连续的十进制值强制归入某一类,因此(在第二行中),我们使用一个 list comprehension 语句来循环所有的预测概率。如果预测概率> = 0.5,那么我们就说是 1 类(也就是我们的肺炎类);否则就是 0 类。我们将这些预测类存储在pred_classes变量中。对于我们的度量,我们还需要生成器中每个值的真实类,可以使用generator.classes来访问。

接下来,我们将显示一个分类报告。首先,我们向用户打印出一份分类报告。接下来,我们打印出 scikit-learn 度量子模块中的classification_report方法的结果。这个方法调用接收真实的类标签、预测的类标签和类名(一个列表)。

我们还将使用confusion_matrix方法生成混淆矩阵(也来自 scikit-learn 的度量子模块)。混淆矩阵方法接受真实的类标签(存储在true_values中)和我们预测的类(存储在pred_classes)。然而,如果我们想真正看到混淆矩阵图(而不是真阳性、真阴性、假阳性和假阴性的数量),我们需要实例化一个ConfusionMatrixDisplay对象(也来自 scikit-learn metrics),传入混淆矩阵调用的结果和一个参数display_labels以及我们的类名。我们将这个 ConfusionMatrixDisplay 调用的结果存储在变量disp中,然后调用disp.plot传入参数values_format=''以确保我们打印出原始数字(默认情况下以科学记数法打印数字)。

接下来,我们要打印出 AUC 和 ROC 图。为此,我们将首先从 scikit 调用roc_curve方法,传入真实的类别标签(true_vals)和存储在preds中的预测概率(因为 ROC 曲线生成过程需要访问原始类别概率)。roc_curve方法返回三个值,一个假阳性率列表,一个假阴性率列表,以及一个与这些率中的每一个相关的概率截止阈值的列表。我们可以使用auc方法(从roc_curve调用传入fprtpr变量)获得 AUC,然后可以打印出结果 AUC 值。最后,我们调用RocCurveDisplay方法来生成 ROC 图。该方法只需要假阳性率和真阳性率列表、AUC 值和模型名称(将显示在 AUC 旁边的图例项中)。然后我们对这个调用的结果调用.plot()并显示最终的图。

现在我们已经设置了打印评估指标的方法,让我们实际调用方法。为了给我们自己最好的评估指标,让我们使用验证损失最低的模型。在大多数情况下,那应该是你拥有的当前模型变量(vgg_modelsmall_model);然而,在某些情况下,最新的模型状态可能不是最好的。然而,回想一下,我们设置了一个ModelCheckpoint回调,它只在每个时期保存一个模型,如果它击败了前一个时期。这意味着我们已经将最好的模型保存到了 Colab 文件系统中,只需要将它加载到内存中!让我们在下面的代码块中实现这一点:

输入【单元格= 18】

small_model_ld = load_model('model_smallnet.hdf5')
vgg_model_ld = load_model('model_vgg.hdf5')

load_model方法来自 tensorflow.keras.models 子模块,并接受一个指向您想要加载的模型的文件路径。如果你回头看看我们的 ModelCheckpoint 定义,它将模型保存在名称model_{model_name}.hdf5下,所以我们只需要传入model_smallnet.hdf5来加载最好的 SmallNet 模型,传入model_vgg.hdf5来加载最好的 VGG16 模型。

我们可以得到如下的模型统计数据:

输入【单元格= 19】

get_stats(vgg_model_ld, test_generator, "vgg")

输出

VGG16 的分类报告参见图 6-11 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-11

VGG16 分类报告

输入【单元格= 20】

get_stats(small_model, test_generator, "smallnet")

输出

SmallNet 的分类报告参见图 6-12 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-12

小型网络分类报告

从该报告中我们可以看出,VGG 网络具有更高的准确度(0.96 对 0.93)、“pn”(肺炎)类别的精确度(0.98 对 0.97)、“pn”类别的召回率(也称为敏感度)(0.96 对 0.93)。VGG 网络的 AUC 为 0.988,略高于 SmallNet 网络的 AUC(0.978)。总的来说,我们会选择 VGG 网络用于将来的使用,因为它在我们的用例中展示了更好的整体统计数据。然而,早期的 Grad-CAM 结果可能会让我们暂停一下,以确定它是否真的具有普遍性(因为除了相关的解剖位置之外,VGG 还关注图像的边缘)。

在这一点上,最好下载你的两个网络。它们将位于 Colab 的 file 选项卡中,并以文件扩展名.hdf5结束(这是 Keras 用来存储模型权重的)。smallnet 模型文件的大小大约为 130 MB(不算太大);然而,VGG16 型号的大小为 1.3 GB,这是巨大的!一般来说,随着网络规模的增加,包含该网络权重的文件大小也会增加(因此,将所有内容加载到计算机内存中需要更长的时间/如果文件非常大,可能会超过计算机的内存限制)。

现在我们已经完成了正式的评估,让我们看看如何在不定义生成器的情况下使用我们的模型。

评估“外部”图像

下面是一个自封闭的代码块,它可以让您输出对任何图像的预测,只要图像被上传到 Colab 文件系统:

输入【单元格= 21】

from tensorflow.keras.preprocessing.image import load_img, img_to_array
from tensorflow.keras.models import load_model
import numpy as np

def predict_image(model, img_path):
  img = img_to_array(load_img(img_path, target_size=(224,224,3)))
  img = img * (1./255)
  img = np.expand_dims(img, axis=0)
  pred = model.predict(img)
  label = 'PNEUMONIA' if pred >= 0.5 else 'NORMAL'
  print("prediction: ", label, "P(Pneumonia): ", pred[0][0])

其中一些导入语句是我们已经导入的,但我只是将它们放在这里,以防您想跳到笔记本的末尾并运行一个经过训练的模型。该方法的第一行将加载一个图像,并将其转换为一个数组,该数组具有我们网络的目标大小(在本例中为 224x224x3)。然后,我们需要将该图像乘以 1/255,因为我们在训练过程中这样做了,并且我们的网络已经基于这些重新缩放的图像进行了学习。然后,我们将扩大图像的维度,以模拟我们的网络的自然输入,这是一个批处理。通过调用np.expand_dims(img, axis=0),我们创建了一个只有一张图片的假批处理。此时,可以将图像输入到model.predict方法中。我们获得预测概率并将其存储在pred中,然后创建一个可解释的标签(根据概率是否为> = 0.5,可以是肺炎,也可以是正常)。最后,我们将这些预测打印到屏幕上。

为了执行该方法,我们运行以下代码:

输入【单元格= 22】

m = load_model('model_smallnet.hdf5')
predict_image(m, 'chest_xray/train/NORMAL/IM-0410-0001.jpeg')

输出

prediction:  NORMAL P(Pneumonia):  0.0043188934

我们需要首先将模型加载到内存中(因此,如果断开连接,您需要将它上传回 Colab 笔记本)。然后我们调用predict_image方法,传入加载的模型和图像路径。

就这样,我们结束了!

需要改进的地方

我们的模型绝不完美(正如我们从 Grad-CAM 结果中看到的)。此外,Kaggle 比赛中使用胸部 x 射线图像的一些参赛者指出,测试数据集有一些错误标记的图像,可能会影响我们的整体准确性统计数据。此外,我们还发现,我们的训练和测试集存在类别不平衡,肺炎图像越来越多。在真实的临床环境中,更可能的是,我们有多个正常的 x 射线图像,但没有很多肺炎图像。我们可以通过向我们的fit_model方法中的model.fit调用传递一个class_weights参数来调整这种类不平衡(您应该查看 Keras 文档了解如何做到这一点!).“类权重”方法会为代表不足的类赋予更大的权重,因此网络表现为好像两个类在数据集中具有相似的分布。

为了解决我们的 Grad-CAM 问题,我们可以尝试继续训练我们的网络,如下所示:

vgg_model_cont, vgg_model_hist_cont = fit_model(train_generator,
    validation_generator, 'vgg', epochs=100, model_fname='model_vgg.hdf5')

这将继续训练我们存储在model_vgg.hdf5中的模型版本,再训练 100 个时期。我们可以持续更长时间;但是,如果您长时间使用 Colab 环境,Colab 将随机停止执行(免费层一次只允许 12 小时的计算)。此外,如果我们的验证损失没有减少,我们的EarlyStopping回调将停止训练,确保我们没有过度适应。

还有一些额外的“愿望清单”项目需要花费相当多的时间来实现,但是对于医疗领域的使用是必要的。首先,我们的输入图像是。jpg 文件;然而,大多数 x 射线文件是以一种称为“DICOM”图像的格式保存的。有 Python 库,比如pydicom,可以让你读取原始的 DICOM 文件。然而,将这些图像转化为可用图像的过程并不简单,因为 DICOM 图像包含的像素值的亮度在 10,000+值范围内,远远超过任何常规图像的 255 max。我们可以使用类似下面的代码行将 DICOM 图像转换成 png,然后在我们的训练管道中使用:

import pydicom
from PIL import Image
import numpy as np
import cv2

def read_dicom_to_png(image_path):
    ds = pydicom.dcmread()
    img = np.array(Image.fromarray(ds.pixel_array))
    mean, std = img.mean(), img.std()
    img = np.clip((img-mean)/std, -2.5, 2.5)
    img = ((img - img.min()) * (1/(img.max() - img.min()) * 255)).astype(np.uint8)
    img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    cv2.imwrite('output.png', img)

粗略地一行一行,我们使用pydicom.dcmread()读入图像。然后,我们将结果像素值(存储在ds.pixel_array中)转换成一个图像对象,并将其转换成一个数组(使用np.array(Image.fromarray(...)))。我们需要完成这一步,因为 Python 中的图像操作库本身并不理解 dicom 像素值。然后,我们得到图像的平均值和标准决策(使用img.mean()img.std())并“裁剪”(即设置边界)任何高于/低于平均值 2.5 个标准偏差的像素值(使用np.clip)。然后,使用我们方法的第五行中的公式,将这些值映射到范围 1–255。然后,我们将使用cv2.cvtColor将图像转换为三通道彩色图像。最后,我们将使用cv2.imwrite将映像写入磁盘。

还有其他需要注意的图像格式(比如 NifTI 图像);但是,它们都将遵循相同的一般步骤,将图像转换为可用于训练网络的内容(例如,使用格式阅读器库加载特定格式的图像,转换为图像数组,裁剪值,重新缩放为 1–255,转换为彩色图像,保存)。

最后,另一个“愿望清单”项目是获得放射科医师对网络预测准确性的意见。理想情况下,如果我们有一组放射科医生也在查看这些图像,我们可以评估评分者之间的可靠性(通过 Kappa 统计),以确定放射科医生之间是否一致,以及这些结果是否与网络预测一致。

概述

在这一章中,我们已经取得了巨大的进步。在设置这一切的过程中,我们学习了如何加载图像数据,清理它,并使用生成器来增加我们的图像数据。然后我们从头开始构建一个卷积神经网络,在这个过程中实现我们在第四章中介绍的概念。通过构建 SmallNet,我们看到了卷积运算对我们的图像大小和我们学习的参数总数的影响。我们还探讨了损失函数和激活函数等概念。然后,我们演示了如何使用回调来监控我们的培训过程,并改变学习率等事情,以鼓励进一步的培训进度,甚至完全停止培训过程。然后,我们继续使用迁移学习将 VGG16 从检测 1000 个不同的图像类别重新用于我们的肺炎分类任务。最后,我们训练了这些网络,并评估了它们的准确度、精确度、召回率和 AUC,以及它们如何“思考”(即,用 Grad-CAM 可视化输出)。

现在,我们已经确切地知道了这些卷积神经网络是如何创建的,以及它们实际上可以做什么,让我们讨论一下 ML 的其他领域,供您自行学习,在医疗领域实现 AI 算法时需要牢记的一般注意事项,以及如何自行继续学习。

七、医疗保健和人工智能的未来

在过去的几个章节中,我们已经浏览了构成“人工智能”的代码,但是所有这些内容只是 ML/AI 世界的一个小样本。尽管您用来实现这些算法的许多工具是相同的(比如 scikit-learn、Keras 和 TensorFlow),但是根据任务的不同,实现会有很大的不同。然而,我们为制作深度学习模型而设置的通用结构(即,让生成器➤定义模型➤定义回调➤训练)确实适用于许多不同的基于深度学习的任务。由于我们没有时间在这一章中谈论所有的事情,我们将讨论如何开始你自己的项目,如何理解错误,以及当你遇到这些错误时该怎么做。

跳出与编码直接相关的概念领域,我们将用三个与在现实世界中开发和部署这些算法时应该考虑的概念相关的部分来结束本章。如今,如何保护患者隐私等概念非常重要,尤其是在医学成像任务需要大量数据才能正确运行的情况下。相应地,我们需要确保人们的信息在这个过程中得到保护/他们同意他们的信息可以用于算法的训练。然后,我们将继续讨论医学领域中与 ML 相关的一些警告,包括如何识别“人工智能蛇油”和防止算法偏差(这是一个描述程序如何证明对一些个人有害的概念)的一般指南。最后,如果你选择从事人工智能研究并将训练好的模型部署到现实世界中,我们将讨论一些你应该报告的事情。

开始你自己的项目

这本书里的大部分内容不仅仅在书本身里有。我在这里列出的一切都来自代码文档、在线问答论坛、从事人工智能工作的公司编写的教程/指南以及我以前的一些课程。不用说,实际上创建真正“原创”的代码是非常困难的(也可以说是毫无意义的)。你打算做的大多数事情都会以某种形式存在于网上;然而,这取决于你是否能在网上找到什么样的工具,以及这些工具如何适应你要解决的问题。

就拿胸透分类问题来说。如果你在网上查找如何使用人工智能在胸部 x 光图像中检测肺炎,你很可能会被导向一些研究论文和数据集所在的 Kaggle 竞赛。你可以浏览一些比赛代码(在大多数 Kaggle 比赛的“代码”标签下可以找到);然而,您可能会发现自己费力地阅读大量文档记录不良的代码,这些代码相当不透明且难以阅读。

相反,你可以试着概括你的问题。我们的输入图像是胸部 x 光片,这一事实可以说没有什么特别的,我们想知道这是不是肺炎。相反,我们可以概括地说:“我想要某种 ML/AI 算法,它可以接受一幅图像并输出一个分类。”如果你在谷歌上输入“ML/AI 图像分类教程 keras ”,你会得到很多有据可查的代码示例和教程,指导你如何制作一个分类器神经网络。如果您在搜索查询中添加“迁移学习”,您可能也会找到关于 VGG16 的内容。如何 a)选择你想学习的教程,b)修改教程代码以适应你的目的,这完全取决于你自己。不管怎样,关键的教训是概括你的问题可以让你找到适合你的用例的东西。

排除故障

好了,现在让我们假设你已经找到了一个向你展示如何使用 VGG16 对图像进行分类的教程,你开始编写代码。然而,当您开始运行时,您开始看到包含单词“error”的消息被打印出来。让我们看一个错误可能是什么样子的例子:

in0 ut

def make_network(network_name):
    print("making model", networkname)

make_network('vgg')

输出

---------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-699ebc7cd358> in <module>()
      2     print("making model", networkname)
      3
----> 4 make_network('vgg')

<ipython-input-1-699ebc7cd358> in make_network(network_name)
      1 def make_network(network_name):
----> 2     print("making model", networkname)
      3
      4 make_network('vgg')

NameError: name 'networkname' is not defined

这条信息似乎相当神秘和令人困惑。然而,让我们来看看这个错误消息告诉我们什么。一般来说,从 Python 中读取错误的最佳方式是从底层开始。在这种情况下,“名称错误:未定义名称‘网络名称’。”好的,所以 Python 似乎认为变量“networkname”不存在(即,它没有被定义)。好的,但是我们不是通过向make_network方法传递一个值来定义它吗?好的,让我们看看“NameError”行上面的下一行。嗯,看起来我们的声明中提到了一个网络名称,但是等等,看起来networknamenetwork_name不一样。看来我们打错字了!如果我们进一步查看错误消息,我们将确切地看到是什么方法调用导致了错误的发生(在本例中,是对make_network('vgg')的调用)。但是,因为我们知道有一个错误是由于打字错误,我们可以找到我们的方法如下:

def make_network(network_name):
    print("making model", network_name)

make_network('vgg')

一切都会按计划进行。

也有一些情况下,自己解释错误消息变得非常困难,甚至不可能。此时,你最好求助于谷歌,尤其是一个名为 StackOverflow 的网站。然而,确实需要一点点的实验来达到你可以在谷歌中输入你的错误,并得到一些有意义的东西。以下是一些关于如何格式化问题的通用指南:

  1. 删除所有特定信息。如果我们在 Google 中键入“name error:name ’ network name ’ not defined ”,我们可能会从处理互联网网络而不是神经网络的堆栈溢出中获得许多“假阳性”结果。这是因为我们的查询太具体了。其他程序员想出完全相同的变量命名方案并弹出完全相同的名称错误的可能性非常低。相反,如果您键入“NameError: name is not defined”,您可能会得到更多的结果,因为所有的 NameError 消息都有相同的单词。

  2. 如果您认为您正在使用的库是相关的,那么也在您的查询中键入它。在这种情况下,我们没有使用任何特殊的库或者在我们的库上调用任何方法,所以我们添加东西没有任何意义。但是,如果我们在运行 scikit-learn 函数后遇到任何其他错误,最好将您的查询格式化为“这里是一般错误消息,scikit learn”

  3. 如果你不能更早地找到任何东西,你可以打开一个关于栈溢出的新问题,希望有人注意到并回答它。为此,您应该打开一个堆栈溢出帐户并提出一个问题。在问题正文中,您应该提供尽可能多的关于您正在运行的脚本的信息,您为调用该脚本/方法做了什么,以及您用来帮助其他人重现问题的任何其他数据。您还应该粘贴弹出的确切错误消息。这些论坛上的大多数人都想帮助别人,但是他们需要提问者做好他们份内的工作,提供足够的信息,这样就没有人会浪费时间来回寻找更多的信息。

在最糟糕的情况下,你可能会发现自己在找出问题所在时没有任何帮助。尽管这种情况很少发生(这更能说明一个事实,那就是你很难用一种能返回搜索结果的方式来表达你的问题),但这仍然是你应该知道如何处理的事情。当我发现自己处于这种情况时,我总是发现重新编码方法或代码段,使用不同的变量名,并且一次只进行一段是非常有益的。它迫使你后退一步,一行一行地检查一切。

还可能出现一些更隐蔽的错误,例如在训练或评估 ML 算法时出现的错误。例如,当我编写代码来评估 SmallNet 和 VGG16 时,我不断获得精度和 AUC,它们与网络最后一个时期的验证 AUC/精度相差甚远(验证 AUC 为 0.97,最终 AUC 为 0.54)。我的堆栈溢出查询包括以下内容:

  • " AUC 计算 sklearn 与 Keras "

  • " Keras 验证 AUC 不同于 sklearn AUC "

  • “测试生成器 AUC Keras 远离验证”

是最后一个问题让我找到了问题的真正解决方案。但我的前两个查询是由最初的想法激发的,即 Keras 在回调指标中计算的 AUC 使用的算法与我们的评估方法中调用的 scikit-learn 计算的 AUC 不同。虽然这些搜索确实产生了一些信息,表明 Keras 和 scikit 实现 AUC 计算的方式不同,但我没有看到任何东西可以解释我所注意到的 AUC 的巨大差异。然后,我发现我使用的评估代码使用了测试生成器,并决定研究生成器是否会打乱数据。这可能导致我们的 AUC 下降到 0.54(相当于随机猜测)。运行的假设是,当我传入测试生成器并调用 model.predict 方法时,测试生成器可能已经打乱了值,然后,当我获得实际的类名时,我将获得新打乱的类名,而不是与我想要预测的图像相关联的原始类名。果然,我遇到了一个堆栈溢出答案,上面写着“确保在生成器上设置了 shuffle=False。”原来生成器默认启用了一个shuffle=True参数,我忘了在代码中的测试集上设置shuffle=False。一旦我这样做了,评估结果与验证结果相匹配!

这是一个逻辑错误的例子。这种错误可能不会产生任何实际的程序错误,但会产生意想不到的结果。处理这些错误的最好方法是从最可能的假设开始,并开始深入越来越具体的假设。同样,重新实现您的代码可能会有所帮助,但是浏览您调用的所有方法的文档也会非常有用。

说到浏览文档,这里有一个你可能会看到的例子(参见图 7-1 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1

文档来自 https://keras.io/api/preprocessing/image/#flowfromdataframe-method

在这里,您可以看到一个如何调用该方法的示例和默认参数列表(列出了具有keyword=value对的任何参数)。如果您没有为这些默认参数手动指定一个新值,程序会认为您想要使用默认值。在代码之后,将会有一个对该方法的简短描述,后面是带有相关描述的参数列表。如果你幸运的话,你可能会发现一些库的作者甚至给出了使用该方法本身的示例代码。

在一个库的文档很少的情况下,其他人可能已经使用过这个库。您可以利用 GitHub(一个托管开源代码的网站)的力量来搜索人们以前对该代码的使用情况。只需在搜索栏中键入方法名称,然后单击结果的“代码”选项卡。任何包含该短语的公开代码都会出现在代码结果中,您可以看到他们实际上是如何使用它的。在这种情况下,我们看到在 GitHub 上查询flow_from_dataframe的结果是与名为Medical_Image_Analysis的存储库(也称为项目)相关联的代码,这可能会让我们更多地查看他们的代码以获得灵感。请参考图 7-2 中您可能会看到的一些示例搜索结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-2

GitHub 代码搜索结果示例

如果你真的被卡住了,你也可以尝试复制代码,但是只有在你真正理解发生了什么的情况下才这样做。此外,如果您打算从 GitHub 获取一些代码,请先访问该库,并检查一个名为“LICENSE.txt”的文件,该文件将准确概述您可以使用该代码做什么,以及您有什么权利复制它(如果有的话)。

既然我们已经花了一些时间在杂草中弄清楚如何自己制作项目并学习如何调试,那么让我们开始考虑人工智能的含义以及在医学中制作基于人工智能的解决方案时必须考虑的主题。

考虑

这绝不是一个人在决定是否追求一个基于人工智能的项目时应该考虑的全面的清单;然而,它触及了目前人工智能领域一些最紧迫的话题。

患者隐私

当我们在医疗环境中谈论患者隐私时,首先想到的概念是 HIPAA。尽管 HIPAA 在安全性和数据匿名化方面倡导的准则可能不适用于所有用例,但仍然应该适用于任何人工智能医疗应用项目。最重要的是,患者隐私保护应该到位,不仅要确保人工智能开发者不会向外界泄露数据,还要确保算法不会学习私人患者数据,这将使其在现实世界中毫无用处。

应该采取哪些确切的保护措施来确保患者隐私?首先,应该从成像标题中去除任何识别成像信息。例如,默认情况下,DICOM 文件将记录有关患者识别号、患者年龄、出生日期和体重的信息,除非另行配置。用于训练神经网络的数据通常会驻留在某人的计算机或服务器上;非匿名的 DICOM 头对于任何想要访问数据的恶意行为者来说都是非常有用的。除了从 DICOM 头和其他相关临床文件中清除信息之外,存储这些数据的服务器和硬盘应该加密,并使用严格控制的访问列表进行密码保护。

就训练一个算法而言,完全有可能算法本身仅仅通过常规的使用,实际上就可以向外界“泄露”信息。一些机器学习算法,如自然语言处理算法(NLP),生成“新”数据作为其输出。例如,可以使用 NLP 算法进行文本生成(例如,生成医疗记录);然而,过度适应其训练数据的 NLP 算法可能只是泄露了该训练数据的一些位,这些位可用于识别个人。仔细选择要训练的算法,并确保信息不被过度拟合,可以证明对解决这个问题是有用的。

说到 ML 算法产生违反直觉的结果的领域,我们来谈谈算法偏差。

算法偏差

算法偏差是指人工智能算法(和其他程序算法)可能产生有偏差的结果。尤其是在种族和社会不平等的背景下,算法偏见是当今训练医学相关机器学习算法时的一个主要问题。为了了解为什么这可能是一个问题,考虑我们可能正在训练一个分类器来检测皮肤上的黑色素瘤的情况。

为了训练我们的网络,我们可以求助于流行的在线鼹鼠图像库。我们下载这个图像库,创建一个分类器,就像我们对肺炎数据集所做的那样,然后评估它的准确性。结果证明准确率超过 90%,这太棒了。然后,我们将这种经过训练的算法带到我们的医院系统/皮肤科医生那里,并要求他们使用它,他们最终报告说,该算法对黑色素瘤状态的预测与皮肤科医生一致,但仅适用于肤色较浅的个人。当患者的皮肤较黑时,该算法完全不能产生有效的结果。我们可能会试图找到原因。也许灯光不好,但这似乎不太可能。然后我们转向原始数据集,发现绝大多数图像来自浅色皮肤的个体。我们可能无意中制造了一个有偏见的算法。这种偏差来自于这样一个事实,即训练数据并不代表真实的患者群体,因此,我们的网络在其训练过程中了解到了这种偏差。

为了帮助克服偏见,我们应该尝试在我们最终的患者群体中对各种人口统计数据的算法评估进行分层。在这种情况下,我们可以使用 Fitzpatrick 皮肤类型量表为每个患者指定一种肤色(I =最亮,VI =最暗),并评估每个类别的分类准确性。在此基础上,我们可以使用 Cochran-Armitage 趋势测试(或您选择的任何其他统计测试)来确定是否有显著的趋势(例如,随着类别数量的增加,准确性下降)。

这种情况不仅仅是理论上的;这是目前现实世界中正在上演的一个问题。谷歌在 2021 年 3 月推出了一款名为“皮肤辅助”的应用。其结果尚未见分晓;然而,研究人员已经开始担心它在肤色较暗的个人中的工作能力,因为用于训练数据集的图像只有(据我们所知)2.7%的 V 型肤色和< 1%的 VI 型肤色。虽然我们不知道这种预测应用程序的最终功效,但我们可以想出一些方法来防止算法偏差影响最终用户。

第一种方法是从一开始就防止我们的数据集中出现偏差。要做到这一点,我们需要注意哪些政策可能会导致有人对数据集做出贡献,而不是做出贡献。例如,如果用于训练算法的数据是自愿提供的,我们应该确保志愿者都能够无障碍地提供数据(例如,缺乏技术、缺乏时间等)。).我们还可以确保我们的数据来自几个不同的医院系统,而不是一家医院,因为一些医院可能位于社会经济或种族不多样化的地区。

防止偏倚的第二种方法是选择性地增加数据集中代表性不足的病例。虽然编程方式超出了本章的范围,但有几个图像增强库(如imgaug)允许您根据图像的特征指定图像增强的频率。如果我们发现一个特定的群体在我们的数据集中代表性不足,我们可以使用这种方法来增加它在整个数据集中的代表性。

防止偏差的最后一种方法是确保评估是在完全独立的维持集上进行的,而不是在属于您最初使用的相同数据的测试集上进行的。虽然这样做更费时间(因为你需要找到多个数据集),但它应该可以在一组从未实际训练过的患者身上验证你的算法的性能,这可能会暴露出你的算法的总体缺点。如果您无法访问另一个数据集,您可以创建一个有限的算法展示,并在向公众发布之前监控人口统计子群的结果,以确定其运行情况。

既然我们已经讨论了算法偏差以及它如何产生意想不到的结果,那么让我们来谈谈基于人工智能的解决方案如何在现实世界中过度承诺而未充分交付。

蛇油+在现实世界中创造信任

人工智能的利弊之一是,有很多资金与采用人工智能的解决方案相关联,特别是在医疗领域。然而,邪恶的行为者已经开始声称他们的技术使用了“人工智能”,而实际上并没有,或者他们甚至走得更远,以人工智能来解决一个已经有着众所周知的算法解决方案的问题。不幸的是,一些医院管理者已经成为这些索赔的牺牲品,最终浪费了数百万美元。

总的来说,在过去的几年里,这些“蛇油”的说法是基于与人工智能相关的“炒作”。能够进行反向图像搜索和面部识别等操作的算法所占据的新闻周期很容易让公众认为人工智能已经在解决“棘手”的问题,并且可以很容易地扩展到医生级别的医疗诊断。虽然人工智能系统实际上超过了一些医生的准确性,但它们只倾向于在非常具体的子任务中这样做(如 x 射线图像上的肺炎检测)。

但是我们实际上如何预测蛇油索赔?我们可以回到本节前面的部分:概括问题。让我们来看一个普遍的人工智能问题,它被认为接近于蛇油(有一点医学上的扭曲):在医生被雇用之前预测医生的工作成功。我们训练算法的输入和输出是什么?输入可能是当前提供者在被雇用时的各种特征,输出可能是他们的工作成就(可能以一段时间后工资增长的百分比来衡量)。有可能已经训练了一种算法来高精度地预测这种情况,但是数据将来自哪里呢?它可能来自一些医疗保健系统,或者更有可能是开发公司本身。在这一点上,我们需要提出关于我们用例中算法有效性的问题(如算法偏差部分所述)。此外,我们需要认识到,该网络只能预测公司/医疗保健系统实际雇用的个人的“准确”结果,因此,它可能不会在其他医疗保健系统/公司中表现良好,这些系统/公司在提供者之间具有不同的宝贵技能。

为了在现实世界中产生信任,有必要尽可能多地尝试和解释人工智能过程。诸如用于任务的一般 ML 算法、用于训练该算法的数据及其评估统计数据等概念都是报告的必要内容。只要有可能,模型输出应该在某种程度上得到解释(就像我们在前一章的 Grad-CAM 结果中显示的那样)。最后一点在医学界尤其受到重视,因为人工智能仍然倾向于被视为“黑匣子”,没有任何程度的解释。

说到解释,让我们来谈谈当你在医疗保健系统中领导一个基于人工智能的项目时,如何从总体上谈论人工智能。

如何谈论人工智能

在某些情况下,你可能会发现自己处于这样的位置,要么直接实现一个人工智能算法,要么负责告诉别人项目的目标是什么。当与程序员一起工作时,尽可能明确是很重要的。让我们通过一个例子来说明如何指定一个问题:我们的胸部 x 光分类问题,从最后一章。如果我们向程序员解释我们想做什么,这些问题是我们需要回答的:

  • 这个项目的目标是什么?

    从胸部 x 光图像预测肺炎状态。

  • 这个人工智能的输入和输出是什么?你对潜在的架构有什么建议吗?

    输入将是胸部 x 射线图像,输出应该是一个单一的世界“正常”或“肺炎”,并附带一个置信度(范围从 0 到 1)。准确显示图像的哪些部分对最终的分类决策贡献最大也是很好的。由于这是一个图像分类问题,所以最好使用卷积神经网络或类似的擅长对图像进行分类的方法。

  • 哪些数据将用于训练这个网络?怎么下载?它的一般格式是什么?这个数据已经被标注了吗?我能相信这些标签吗?

    我们将使用 Kaggle 胸部 x 光数据集。这些可以从 Kaggle 竞赛网站下载(发给他们一个链接)。这些图像是。jpg 文件,在它们可以用作神经网络的输入之前不需要任何额外的处理(如果它们是 DICOM 图像,您应该在这里指定)。它们已经被标记为正常与肺炎,并且图像已经在标记有适当类别的文件夹中。在大多数情况下,标签是可信的,但我们可能需要另一位医生来查看测试数据,并确保它是准确的。

  • 我们想要优化什么指标?

    我们希望优化准确性,但也希望确保我们有非常少的假阴性(即,我们希望有非常高的灵敏度),因为错过肺炎病例是有害的。

  • 这个网络将在哪里运行?我可以联系谁来让它在这种环境下运行?那个平台的约束是什么?

    理想情况下,我们希望将这个网络集成到我们的 PACS 系统中,这样它就可以立即为我们提供预测,而无需我们执行单独的脚本。X 人可以告诉你需要做什么来把你的输出变成一种可以理解的格式。(注意这在上一章中没有涉及;然而,这也是开发人员的一个主要痛点,因为不是每个人都会运行 Colab 笔记本来从您的网络获得输出。在某些情况下,PACS 系统可能不允许执行单独的脚本,这使得程序员的工作变得困难。在这种情况下,最好妥协一下,说你愿意培训医生如何使用开发者制作的任何定制软件。)

  • 你需要这个网络继续接受训练吗?您将如何确定要添加哪些新的训练样本?

    是的,我们希望这个网络继续得到训练。理想情况下,任何使用该模型的应用程序都应该标记出模型与医生不一致的结果,并使用该案例作为训练样本。(注意,我们在上一章中没有明确涉及这一点,但是您也可以继续在不同的数据集上训练网络;你只需要一个新的火车发电机就行了)。

一旦你回答了这些问题,你就应该能够指出你到底需要为你的项目工作的开发人员做些什么。编程过程中出现的许多开发难点通常是由于误解了预期,明确了解这些解决方案可以让您为开发人员提供足够的信息来完成他们的任务。

包裹

正如我们在本章和前面的章节中所看到的,人工智能并不是真正的魔法,我们可以理解这些程序是如何学习并产生它们所做的结果的。虽然这本书没有太多涉及人工智能的潜力,但编程/计算机科学主题中涵盖的概念、一些基本的 ML 算法类型以及 ML 和深度学习算法的实现细节将为你开始自己的探索提供足够的基础。即使这本书涵盖了两个主要项目,医学研究的整个世界还是向你敞开了。使用 scikit-learn 和 PyCaret,您可以评估不同的数据集并尝试优化分类任务。使用 TensorFlow 和 Keras,您可以使用迁移学习对医学成像文献中尚未涉及的疾病进行图像分类。有了上一章建立的技能,你也将领先于你的同龄人,知道在评估呈现给你的人工智能解决方案时要注意什么。当你自己承担一个基于人工智能的项目时,你甚至有一些准备什么答案的基本例子。

总的来说,这本书旨在作为进一步知识增长的种子,我希望你可以继续学习如何使用一些基于人工智能的算法来造福医疗环境中的患者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值