【第一周】吴恩达团队AI for Medical Diagnosis大作业

系列文章目录

【第一周】吴恩达团队AI for Medical Diagnosis课程笔记_十三豆腐脑的博客-CSDN博客


目录

系列文章目录

前言

二、加载数据集

1.加载数据

2.防止数据泄露

3.准备图像

三、建立模型

1.处理类别不均问题

2.DenseNet121 

四、训练模型(可选)

1.在更大的数据集上训练

五、预测和评估

1.ROC 曲线和 AUROC

2. 使用 GradCAM 可视化学习

总结


前言

第一周大作业:基于深度学习的胸部X光医学诊断

欢迎来到人工智能医学诊断的第一个任务!

在本作业中,您将通过使用 Keras 构建最先进的胸部 X 射线分类器来探索医学图像诊断。

该作业将介绍构建和评估此深度学习分类器模型的一些步骤。 特别是,您将:

1.预处理和准备真实世界的 X 射线数据集。
2.使用迁移学习重新训练 DenseNet 模型以进行 X 射线图像分类。
3.学习一种处理类不平衡的技术
4.通过计算 ROC(接受者操作特征)曲线的 AUC(曲线下面积)来衡量诊断性能。
5.使用 GradCAM 可视化模型活动。

在完成此作业时,您将了解以下主题:

1.数据准备
2.可视化数据。
3.防止数据泄露。
4.模型开发
5.解决阶级不平衡。
6.使用迁移学习利用预训练模型。
7.评估
8.AUC 和 ROC 曲线。


一、导入包和函数

我们将使用以下软件包:

numpy 和 pandas 是我们用来操作数据的工具
matplotlib.pyplot 和 seaborn 将用于生成可视化图
util 将提供已为此分配提供的本地定义的实用程序函数
我们还将使用 keras 框架中的几个模块来构建深度学习模型。

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

from keras.preprocessing.image import ImageDataGenerator
from keras.applications.densenet import DenseNet121
from keras.layers import Dense, GlobalAveragePooling2D
from keras.models import Model
from keras import backend as K

from keras.models import load_model

import util
from public_tests import *
from test_utils import *

import tensorflow as tf
tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)

二、加载数据集

对于这项任务,我们将使用 ChestX-ray8 数据集,其中包含 32,717 名独特患者的 108,948 张正面 X 射线图像。

数据集中的每个图像都包含多个文本挖掘标签,识别 14 种不同的病理状况。
这些反过来又可以被医生用来诊断 8 种不同的疾病。
我们将使用这些数据开发一个模型,该模型将为 14 种标记病理中的每一种提供二元分类预测。
换句话说,它将预测每种病理的“阳性”或“阴性”。
你可以在这里免费下载整个数据集。

我们为您提供了大约 1000 个图像子集。
这些可以在 IMAGE_DIR 变量中存储的文件夹路径中访问。
该数据集包含一个 CSV 文件,该文件为每个 X 射线提供标签。

为了让您的工作更轻松,我们已经处理了小样本的标签并生成了三个新文件以帮助您入门。这三个文件是:

nih/train-small.csv:我们数据集中的 875 张图像用于训练。
nih/valid-small.csv:我们数据集中的 109 张图像用于验证。
nih/test.csv:我们数据集中的 420 张图像用于测试。
该数据集已根据我们 14 种病理中的 5 种的 5 种不同放射科医生的共识进行注释:

Consolidation
Edema
Effusion
Cardiomegaly
Atelectasis

关于“类”含义的侧边栏
值得注意的是,这些讨论中以多种方式使用了“类”这个词。

我们有时将数据集中标记的 14 种病理状况中的每一种称为一个类别。
但是对于这些病理中的每一种,我们都试图预测某种情况是存在(即阳性结果)还是不存在(即阴性结果)。
这两个可能的“正”或“负”标签(或 1 或 0 的数值等价物)通常也称为类。
此外,我们还使用该术语来指代诸如 ImageDataGenerator 之类的软件代码“类”。
不过,只要您了解这一切,就不会造成任何混淆,因为“类”一词通常在使用它的上下文中是很清楚的。

1.加载数据

让我们使用 pandas 库打开这些文件

train_df = pd.read_csv("data/nih/train-small.csv")
valid_df = pd.read_csv("data/nih/valid-small.csv")

test_df = pd.read_csv("data/nih/test.csv")

train_df.head()
labels = ['Cardiomegaly', 
          'Emphysema', 
          'Effusion', 
          'Hernia', 
          'Infiltration', 
          'Mass', 
          'Nodule', 
          'Atelectasis',
          'Pneumothorax',
          'Pleural_Thickening', 
          'Pneumonia', 
          'Fibrosis', 
          'Edema', 
          'Consolidation']

2.防止数据泄露

值得注意的是,我们的数据集包含每个患者的多个图像。 例如,当患者在医院就诊期间的不同时间拍摄了多张 X 射线图像时,就可能出现这种情况。 在我们的数据拆分中,我们确保拆分是在患者级别完成的,因此训练、验证和测试数据集之间没有数据“泄漏”。

练习 1 - 检查泄漏
在下面的单元格中,编写一个函数来检查两个数据集之间是否存在泄漏。 我们将使用它来确保测试集中没有患者也出现在训练集中或验证集中。

提示
利用 python 的 set.intersection() 函数。
为了符合自动评分者的期望,请以 df1_patients_unique 开头的代码行...[在此处继续您的代码]

# UNQ_C1 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
def check_for_leakage(df1, df2, patient_col):
    """
    Return True if there any patients are in both df1 and df2.

    Args:
        df1 (dataframe): dataframe describing first dataset
        df2 (dataframe): dataframe describing second dataset
        patient_col (str): string name of column with patient IDs
    
    Returns:
        leakage (bool): True if there is leakage, otherwise False
    """

    ### START CODE HERE (REPLACE INSTANCES OF 'None' with your code) ###
    
    df1_patients_unique = set(df1[patient_col].values)
    df2_patients_unique = set(df2[patient_col].values)
    
    patients_in_both_groups = list(df1_patients_unique.intersection(df2_patients_unique))

    # leakage contains true if there is patient overlap, otherwise false.
    leakage = True if len(patients_in_both_groups) >= 1 else False # boolean (true if there is at least 1 patient in both groups)
    
    ### END CODE HERE ###
    
    return leakage
### do not edit this code cell    
check_for_leakage_test(check_for_leakage)
print("leakage between train and valid: {}".format(check_for_leakage(train_df, valid_df, 'PatientId')))
print("leakage between train and test: {}".format(check_for_leakage(train_df, test_df, 'PatientId')))
print("leakage between valid and test: {}".format(check_for_leakage(valid_df, test_df, 'PatientId')))

3.准备图像

准备好数据集拆分后,我们现在可以继续设置模型以使用它们。

为此,我们将使用 Keras 框架中现成的 ImageDataGenerator 类,它允许我们为数据帧中指定的图像构建“生成器”。
此类还提供对基本数据增强的支持,例如图像的随机水平翻转。
我们还使用生成器来转换每批中的值,使其平均值为 0,标准差为 1。
这将通过标准化输入分布来促进模型训练。
生成器还通过在所有通道上重复图像中的值,将我们的单通道 X 射线图像(灰度)转换为三通道格式。
我们会想要这个,因为我们将使用的预训练模型需要三通道输入。
由于主要是阅读和理解 Keras 文档,因此我们为您实现了生成器。有几点需要注意:

我们对数据的均值和标准差进行归一化
我们在每个 epoch 之后打乱输入。
我们将图像尺寸设置为 320 像素 x 320 像素

def get_train_generator(df, image_dir, x_col, y_cols, shuffle=True, batch_size=8, seed=1, target_w = 320, target_h = 320):
    """
    Return generator for training set, normalizing using batch
    statistics.

    Args:
      train_df (dataframe): dataframe specifying training data.
      image_dir (str): directory where image files are held.
      x_col (str): name of column in df that holds filenames.
      y_cols (list): list of strings that hold y labels for images.
      batch_size (int): images per batch to be fed into model during training.
      seed (int): random seed.
      target_w (int): final width of input images.
      target_h (int): final height of input images.
    
    Returns:
        train_generator (DataFrameIterator): iterator over training set
    """        
    print("getting train generator...") 
    # normalize images
    image_generator = ImageDataGenerator(
        samplewise_center=True,
        samplewise_std_normalization= True)
    
    # flow from directory with specified batch size
    # and target image size
    generator = image_generator.flow_from_dataframe(
            dataframe=df,
            directory=image_dir,
            x_col=x_col,
            y_col=y_cols,
            class_mode="raw",
            batch_size=batch_size,
            shuffle=shuffle,
            seed=seed,
            target_size=(target_w,target_h))
    
    return generator

为验证集和测试集构建单独的生成器
现在我们需要为验证和测试数据构建一个新的生成器。

为什么我们不能使用与训练数据相同的生成器?

回顾一下我们为训练数据编写的生成器。

它对每个批次的每个图像进行标准化,这意味着它使用批次统计信息。
我们不应该对测试和验证数据执行此操作,因为在现实生活场景中,我们不会一次处理一批传入的图像(我们一次处理一个图像)。
了解每批测试数据的平均值将有效地为我们的模型带来优势。
模型不应该有关于测试数据的任何信息。
我们需要做的是使用从训练集计算的统计数据对传入的测试数据进行归一化。

我们在下面的函数中实现了这一点。
有一个技术说明。理想情况下,我们希望使用整个训练集来计算样本均值和标准差。
然而,由于这是非常大的,这将非常耗时。
出于时间考虑,我们将随机抽取数据集样本并计算样本均值和样本标准差。

def get_test_and_valid_generator(valid_df, test_df, train_df, image_dir, x_col, y_cols, sample_size=100, batch_size=8, seed=1, target_w = 320, target_h = 320):
    """
    Return generator for validation set and test set using 
    normalization statistics from training set.

    Args:
      valid_df (dataframe): dataframe specifying validation data.
      test_df (dataframe): dataframe specifying test data.
      train_df (dataframe): dataframe specifying training data.
      image_dir (str): directory where image files are held.
      x_col (str): name of column in df that holds filenames.
      y_cols (list): list of strings that hold y labels for images.
      sample_size (int): size of sample to use for normalization statistics.
      batch_size (int): images per batch to be fed into model during training.
      seed (int): random seed.
      target_w (int): final width of input images.
      target_h (int): final height of input images.
    
    Returns:
        test_generator (DataFrameIterator) and valid_generator: iterators over test set and validation set respectively
    """
    print("getting train and valid generators...")
    # get generator to sample dataset
    raw_train_generator = ImageDataGenerator().flow_from_dataframe(
        dataframe=train_df, 
        directory=IMAGE_DIR, 
        x_col="Image", 
        y_col=labels, 
        class_mode="raw", 
        batch_size=sample_size, 
        shuffle=True, 
        target_size=(target_w, target_h))
    
    # get data sample
    batch = raw_train_generator.next()
    data_sample = batch[0]

    # use sample to fit mean and std for test set generator
    image_generator = ImageDataGenerator(
        featurewise_center=True,
        featurewise_std_normalization= True)
    
    # fit generator to sample from training data
    image_generator.fit(data_sample)

    # get test generator
    valid_generator = image_generator.flow_from_dataframe(
            dataframe=valid_df,
            directory=image_dir,
            x_col=x_col,
            y_col=y_cols,
            class_mode="raw",
            batch_size=batch_size,
            shuffle=False,
            seed=seed,
            target_size=(target_w,target_h))

    test_generator = image_generator.flow_from_dataframe(
            dataframe=test_df,
            directory=image_dir,
            x_col=x_col,
            y_col=y_cols,
            class_mode="raw",
            batch_size=batch_size,
            shuffle=False,
            seed=seed,
            target_size=(target_w,target_h))
    return valid_generator, test_generator

准备好生成器功能后,让我们为训练数据和每个测试和验证数据集制作一个生成器。

IMAGE_DIR = "data/nih/images-small/"
train_generator = get_train_generator(train_df, IMAGE_DIR, "Image", labels)
valid_generator, test_generator= get_test_and_valid_generator(valid_df, test_df, train_df, IMAGE_DIR, "Image", labels)

让我们看看生成器在训练和验证期间为我们的模型提供了什么。 我们可以通过调用 __get_item__(index) 函数来做到这一点: 

x, y = train_generator.__getitem__(0)
plt.imshow(x[0]);

三、建立模型

现在我们将继续进行模型训练和开发。 不过,在实际训练神经网络之前,我们需要应对一些实际挑战。 首先是类别不平衡。

1.处理类别不均问题

使用医疗诊断数据集的挑战之一是此类数据集中存在的类别极度不平衡。 让我们绘制数据集中每个标签的频率:

plt.xticks(rotation=90)
plt.bar(x=labels, height=np.mean(train_generator.labels, axis=0))
plt.title("Frequency of Each Class")
plt.show()

从该图中我们可以看出,阳性病例的流行率在不同的病理中存在显着差异。 (这些趋势也反映了完整数据集中的趋势。)

疝气病理失衡最大,阳性训练病例比例约为0.2%。
但即使是不平衡量最少的浸润病理学,也只有 17.5% 的训练案例被标记为阳性。
理想情况下,我们将使用均衡的数据集训练我们的模型,以便正负训练案例对损失的贡献相同。

如果我们使用具有高度不平衡数据集的正常交叉熵损失函数,正如我们在这里看到的那样,那么算法将被激励优先考虑多数类(即在我们的例子中为负数),因为它对损失的贡献更大。

练习 2 - 计算类频率
完成下面的函数来计算我们数据集中每个标签的频率。

提示
使用 numpy.sum(a, axis=),并选择axis(0 或 1)

# UNQ_C2 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
def compute_class_freqs(labels):
    """
    Compute positive and negative frequences for each class.

    Args:
        labels (np.array): matrix of labels, size (num_examples, num_classes)
    Returns:
        positive_frequencies (np.array): array of positive frequences for each
                                         class, size (num_classes)
        negative_frequencies (np.array): array of negative frequences for each
                                         class, size (num_classes)
    """
    ### START CODE HERE (REPLACE INSTANCES OF 'None' with your code) ###
    
    # total number of patients (rows)
    N = labels.shape[0]
    
    positive_frequencies = np.sum(labels==1, axis=0)/N
    negative_frequencies = np.sum(labels==0, axis=0)/N

    ### END CODE HERE ###
    return positive_frequencies, negative_frequencies
### do not edit this code cell       
compute_class_freqs_test(compute_class_freqs)
freq_pos, freq_neg = compute_class_freqs(train_generator.labels)
freq_pos

让我们可视化每种病理的这两个贡献率彼此相邻: 

data = pd.DataFrame({"Class": labels, "Label": "Positive", "Value": freq_pos})
data = data.append([{"Class": labels[l], "Label": "Negative", "Value": v} for l,v in enumerate(freq_neg)], ignore_index=True)
plt.xticks(rotation=90)
f = sns.barplot(x="Class", y="Value", hue="Label" ,data=data)

正如我们在上图中看到的,正例的贡献明显低于负例的贡献。 但是,我们希望贡献是平等的。 这样做的一种方法是将每个类的每个示例乘以特定于类的权重因子 wpos 和 wneg ,以便每个类的整体贡献相同。

pos_weights = freq_neg
neg_weights = freq_pos
pos_contribution = freq_pos * pos_weights 
neg_contribution = freq_neg * neg_weights


data = pd.DataFrame({"Class": labels, "Label": "Positive", "Value": pos_contribution})
data = data.append([{"Class": labels[l], "Label": "Negative", "Value": v} 
                        for l,v in enumerate(neg_contribution)], ignore_index=True)
plt.xticks(rotation=90)
sns.barplot(x="Class", y="Value", hue="Label" ,data=data);

练习 3 - 获得加权损失
填写下面的 weighted_loss 函数,返回一个计算每个批次的加权损失的损失函数。 回想一下,对于多类损失,我们将每个单独类的平均损失相加。 请注意,我们还希望在获取它们的日志之前向预测值添加一个小值 ϵ 。 这只是为了避免在预测值恰好为零时会出现的数值错误。

提示:
请使用 Keras 函数计算平均值和对数。

Keras.mean
Keras.log

# UNQ_C3 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
def get_weighted_loss(pos_weights, neg_weights, epsilon=1e-7):
    """
    Return weighted loss function given negative weights and positive weights.

    Args:
      pos_weights (np.array): array of positive weights for each class, size (num_classes)
      neg_weights (np.array): array of negative weights for each class, size (num_classes)
    
    Returns:
      weighted_loss (function): weighted loss function
    """
    def weighted_loss(y_true, y_pred):
        """
        Return weighted loss value. 

        Args:
            y_true (Tensor): Tensor of true labels, size is (num_examples, num_classes)
            y_pred (Tensor): Tensor of predicted labels, size is (num_examples, num_classes)
        Returns:
            loss (float): overall scalar loss summed across all classes
        """
        # initialize loss to zero
        loss = 0.0
        
        ### START CODE HERE (REPLACE INSTANCES OF 'None' with your code) ###

        for i in range(len(pos_weights)):
            # for each class, add average weighted loss for that class 
            loss += -K.mean(pos_weights[i]*y_true[:,i]*K.log(y_pred[:,i]+epsilon))-K.mean(neg_weights[i]*(1-y_true[:,i])*K.log(1-y_pred[:,i]+epsilon)) #complete this line
        return loss
    
        ### END CODE HERE ###
    return weighted_loss
# test with a large epsilon in order to catch errors. 
# In order to pass the tests, set epsilon = 1
epsilon = 1

### do not edit anything below
sess = K.get_session()
get_weighted_loss_test(get_weighted_loss, epsilon, sess)

2.DenseNet121 

接下来,我们将使用一个预训练的 DenseNet121 模型,我们可以直接从 Keras 加载该模型,然后在其上添加两层:

一个 GlobalAveragePooling2D 层,用于从 DenseNet121 获取最后一个卷积层的平均值。
一个具有 sigmoid 激活的 Dense 层,用于获取我们每个类的预测 logits。
我们可以通过在 compile() 函数中指定损失参数来为模型设置自定义损失函数。

# create the base pre-trained model
base_model = DenseNet121(weights='models/nih/densenet.hdf5', include_top=False)

x = base_model.output

# add a global spatial average pooling layer
x = GlobalAveragePooling2D()(x)

# and a logistic layer
predictions = Dense(len(labels), activation="sigmoid")(x)

model = Model(inputs=base_model.input, outputs=predictions)
model.compile(optimizer='adam', loss=get_weighted_loss(pos_weights, neg_weights))

四、训练模型(可选)

随着我们的模型准备好训练,我们将使用 Keras 中的 model.fit() 函数来训练我们的模型。

我们正在对数据集的一小部分(~1%)进行训练。
所以我们此时关心的是确保训练集上的损失在减少。
由于训练可能需要相当长的时间,出于教学目的,我们选择不在此处训练模型,而是在下一节加载一组预训练的权重。但是,您可以使用下面显示的代码在您的机器或 Colab 中本地练习训练模型。

注意:请勿在 Coursera 平台上运行以下代码,因为它会超出平台的内存限制。

用于训练模型的 Python 代码:

history = model.fit_generator(train_generator, 
                              validation_data=valid_generator,
                              steps_per_epoch=100, 
                              validation_steps=25, 
                              epochs = 3)

plt.plot(history.history['loss'])
plt.ylabel("loss")
plt.xlabel("epoch")
plt.title("Training Loss Curve")
plt.show()

1.在更大的数据集上训练

鉴于原始数据集大小超过 40GB,并且整个数据集的训练过程需要几个小时,我们已经为您在配备 GPU 的机器上训练了模型,并提供了我们模型的权重文件(批量大小为 32而是)用于此作业的其余部分。

我们的预训练模型的模型架构完全相同,但我们使用了一些有用的 Keras“回调”来进行训练。请在闲暇时花时间阅读这些回调,因为它们对于管理长期运行的培训课程非常有用:

您可以使用 ModelCheckpoint 回调来监控模型的 val_loss 指标并在该点保留模型的快照。
您可以使用 TensorBoard 使用 Tensorflow Tensorboard 实用程序来实时监控您的运行。
您可以使用 ReduceLROnPlateau 缓慢衰减模型的学习率,因为它在 val_loss 等指标上停止变得更好,以便在训练的最后步骤中微调模型。
当您的模型在验证损失中停止变得更好时,您可以使用 EarlyStopping 回调来停止训练作业。您可以设置一个耐心值,该值是模型在训练终止后没有改进的时期数。此回调还可以在模型训练结束时方便地恢复最佳指标的权重。
您可以在此处阅读有关这些回调和其他有用的 Keras 回调的信息。

现在让我们将预训练的权重加载到模型中:

model.load_weights("models/nih/pretrained_model.h5")

五、预测和评估

现在我们有了一个模型,让我们使用我们的测试集对其进行评估。 我们可以方便地使用 predict_generator 函数为我们的测试集中的图像生成预测。

注意:以下单元格可能需要大约 4 分钟才能运行。

predicted_vals = model.predict_generator(test_generator, steps = len(test_generator))

1.ROC 曲线和 AUROC


我们将在以后的几周内更详细地介绍模型评估的主题,但现在我们将通过 ROC(接收器操作特征)曲线计算一个称为 AUC(曲线下面积)的指标。 这也称为 AUROC 值,但您会看到所有三个术语都与该技术有关,并且通常几乎可以互换使用。

现在,为了解释该图,您需要知道的是,一条更靠左且顶部的曲线下方有更多的“区域”,并表明该模型的性能更好。

我们将使用 util.py 中为您提供的 util.get_roc_curve() 函数。 查看此函数并注意使用 sklearn 库函数为我们的模型生成 ROC 曲线和 AUROC 值。

roc_curve
roc_auc_score

auc_rocs = util.get_roc_curve(labels, predicted_vals, test_generator)

2. 使用 GradCAM 可视化学习

在医学中使用深度学习的挑战之一是,与传统的机器学习模型(例如线性模型)相比,用于神经网络的复杂架构使得它们更难解释。

旨在提高计算机视觉任务模型可解释性的最常见方法之一是使用类激活图 (CAM)。

类激活图有助于理解模型在对图像进行分类时“寻找”的位置。
在本节中,我们将使用 GradCAM 的技术生成热图,突出显示图像中的重要区域,以预测病理状况。

这是通过提取每个预测类的梯度,流入我们模型的最终卷积层来完成的。查看 util.py 中为您提供的 util.compute_gradcam,了解 Keras 框架是如何完成的。
值得一提的是,GradCAM 并没有对每个分类概率的推理提供完整的解释。

然而,它仍然是“调试”我们的模型和增强我们的预测的有用工具,以便专家可以验证预测确实是由于模型专注于图像的正确区域。
首先,我们将加载小型训练集和设置,以查看具有最高 AUC 测量值的 4 个类。

df = pd.read_csv("data/nih/train-small.csv")
IMAGE_DIR = "data/nih/images-small/"

# only show the labels with top 4 AUC
labels_to_show = np.take(labels, np.argsort(auc_rocs)[::-1])[:4]

现在让我们看一些具体的图像。

util.compute_gradcam(model, '00008270_015.png', IMAGE_DIR, df, labels, labels_to_show)

util.compute_gradcam(model, '00011355_002.png', IMAGE_DIR, df, labels, labels_to_show)

util.compute_gradcam(model, '00029855_001.png', IMAGE_DIR, df, labels, labels_to_show)

util.compute_gradcam(model, '00005410_000.png', IMAGE_DIR, df, labels, labels_to_show)

 


总结

提示:这里对文章进行总结:

例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值