前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >医学图像分析的深度学习

医学图像分析的深度学习

作者头像
代码医生工作室
发布2019-06-21 17:29:15
1.3K0
发布2019-06-21 17:29:15
举报
文章被收录于专栏:相约机器人相约机器人

作者 | Pawan Jain

来源 | Medium

编辑 | 代码医生团队

初学者的教程,在OCT视网膜图像上的pytorch中使用vgg16架构实现迁移学习。

深度学习有可能通过对人类专家进行难以分类并快速检查大量图像来彻底改变疾病诊断和管理。

关于数据集

视网膜OCT图像的该数据集是从Kaggle数据集获得的。有四类图像CNV,DME,DRUSEN和NORMAL

https://www.kaggle.com/paultimothymooney/kermany2018

  • (最左侧)脉络膜新生血管(CNV),具有新生血管膜(白色箭头)和相关的视网膜下液(箭头)。
  • (左中)糖尿病性黄斑水肿(DME)与视网膜增厚相关的视网膜内液(箭头)。
  • (中右)早期AMD中存在多个玻璃疣(箭头)。
  • (最右侧)具有保留的中心凹轮廓且没有任何视网膜液/水肿的正常视网膜。

关于OCT

光学相干断层扫描(OCT)是一种使用相干光捕获生物组织的高分辨率图像的成像技术。

  • OCT被眼科医生大量使用以获得眼睛视网膜的高分辨率图像。眼睛的视网膜更像是相机中的胶片。
  • OCT图像可用于诊断许多视网膜相关的眼病。
  • 在某些情况下,单独的OCT可能会产生诊断(例如黄斑裂孔)。然而在其他疾病,特别是视网膜血管疾病中,订购额外的测试(例如荧光血管造影)可能是有帮助的。

探索数据集

试着看看每个类别中的图像数量和图像的大小。

代码语言:javascript
复制
for d in os.listdir(traindir):
    categories.append(d)
 
    # Number of each image
    train_imgs = os.listdir(traindir + d)
    valid_imgs = os.listdir(validdir + d)
    test_imgs = os.listdir(testdir + d)
    n_train.append(len(train_imgs))
    n_valid.append(len(valid_imgs))
    n_test.append(len(test_imgs))
 
# Dataframe of categories
cat_df = pd.DataFrame({'category': categories,'n_train': n_train,
                       'n_valid': n_valid, 'n_test': n_test})
 
cat_df.sort_values('n_train', ascending=False, inplace=True)
cat_df.head()

得到一个数据框,其中包含测试,训练和验证文件夹中每个类别的图像计数,通过它可以获得关于数据集的一些基本直觉

  • 验证数据集中只有9个图像(极少数)
  • 我们有大约37k的火车图像CNV,26k NORMAL 和11k以及8k DME和DRUSEN

图像预处理

要为网络准备图像,必须将它们调整为224 x 224,并通过减去平均值并除以标准偏差来标准化每个颜色通道。还将在此阶段增加训练数据。这些操作是使用图像完成的,图像transforms为神经网络准备数据。

代码语言:javascript
复制
for i in train_imgs:
        img_categories.append(d)
        img = Image.open(traindir + d + '/' + i)
        img_array = np.array(img)
        # Shape
        hs.append(img_array.shape[0])
        ws.append(img_array.shape[1])
        
sns.kdeplot(img_dsc['height']['mean'], label='Average Height')
sns.kdeplot(img_dsc['width']['mean'], label='Average Width')

这有助于可视化训练集中图像的宽度和高度范围

当在预先训练的网络中使用图像时,必须将它们重塑为224 x 224.这是图像的大小,因此是模型所期望的。大于此的图像将被截断,而较小的图像将被插值。

数据扩充

由于图像数量有限,可以使用图像增强来人为地增加网络“看到”的图像数量。这意味着,对于训练,会随机调整大小并裁剪图像,并将其水平翻转。对每个时期应用不同的随机变换(在训练时),因此网络有效地看到同一图像的许多不同版本。

  • Tensors在标准化之前,所有数据也都转换为Torch 。
  • 验证和测试数据不会增加,但只会调整大小并进行标准化。
代码语言:javascript
复制
image_transforms = {
    # Train uses data augmentation
    'train':
    transforms.Compose([
        transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)),
        transforms.RandomRotation(degrees=15),
        transforms.ColorJitter(), #randomly change brightness,hue and saturaion of images
        transforms.RandomHorizontalFlip(),transforms.CenterCrop(size=224),
        transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225])]),
    
    # Validation and test does not use augmentation
    'val':
    transforms.Compose([
        transforms.Resize(size=256),transforms.CenterCrop(size=224),
        transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
  
    'test':
    transforms.Compose([
        transforms.Resize(size=256),transforms.CenterCrop(size=224),
        transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
}

数据迭代器

为避免一次将所有数据加载到内存中,使用训练DataLoaders。

  • 首先,从图像文件夹创建一个数据集对象,然后将它们传递给 DataLoader。
  • 在训练时,DataLoader将从磁盘加载图像,应用转换,并产生批处理。
  • 为了训练和验证,将遍历相应的所有批次DataLoader。

一个关键方面是shuffle将数据传递给网络之前的数据。这意味着图像类别的排序在每次通过数据时都会发生变化(一次通过数据是一个训练时期)。

代码语言:javascript
复制
data = {'train':datasets.ImageFolder(root=traindir, transform=image_transforms['train']),
        'val':datasets.ImageFolder(root=validdir, transform=image_transforms['val']),
        'test':datasets.ImageFolder(root=testdir, transform=image_transforms['test'])}
 
# Dataloader iterators
dataloaders = {'train': DataLoader(data['train'], batch_size=batch_size, shuffle=True),
               'val': DataLoader(data['val'], batch_size=batch_size, shuffle=True),
               'test': DataLoader(data['test'], batch_size=batch_size, shuffle=True)}

使用预先训练的模型(VGG-16)

预训练背后的想法CNN是与许多图像识别任务相关的提取特征的早期卷积层。后面的完全连接的图层通过学习更高级别的特征来专门处理特定数据集。

因此可以使用已经训练过的卷积层,同时只训练自己的数据集上的完全连接的层。事实证明,经过预先训练的网络可以在各种任务中取得相当的成功,并且可以显着缩短训练时间,并且通常可以提高性能。

VGG-16架构

这classifier是将训练的模型的一部分。然而对于vgg,只需要在分类器中训练最后几层,甚至不需要训练所有完全连接的层。

vgg16的分类器部分,具有1000个out_features

  • 通过设置requires_grad为冻结网络中的所有现有图层False
  • 为了构建自定义分类器,使用nn.Sequential()允许一个接一个地指定每个层的模块
  • 最终输出将是对数概率,可以在负对数似然丢失(NLLL)中使用。
  • 只需将整个模型移动到GPU上即可

已经跳过了上面部分的代码,可以在Github存储库或Kaggle内核中查看它们。

https://github.com/pawangeek/pytorch-notebooks/tree/master/Retinal-image-classification

https://www.kaggle.com/kernels/scriptcontent/15436113/download

代码语言:javascript
复制
otal_params = sum(p.numel() for p in model.parameters())
print(f'{total_params:,} total parameters.')
 
total_trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'{total_trainable_params:,} training parameters.')

共有135,310,404个参数。

1,049,860个训练参数。

将类映射到索引

为了跟踪模型所做的预测,创建了类到索引和索引到类的映射。这将知道给定预测的实际类。

代码语言:javascript
复制
model.class_to_idx = data['train'].class_to_idx
model.idx_to_class = {idx: class_
                      for class_, idx in model.class_to_idx.items()}
 
list(model.idx_to_class.items())
代码语言:javascript
复制
[(0,'CNV'),(1,'DME'),(2,'DRUSEN'),(3,'NORMAL')]

所以完整模型是经过预先训练的vgg + custom classifier。这是一个很长的模型,不可能在这里发布。不过设法通过使用来自上一个自定义模块的快照torchsummary

模型的自定义分类器

  • 128是批量大小,batch_size如果这不适合你的GPU ,你可能需要减少它
  • 有4个类进行分类,这在最后一层非常清楚

训练损失和优化

损失(标准):跟踪损失本身以及相对于模型参数(重量)的损失梯度

优化器:使用渐变更新参数(重量)

代码语言:javascript
复制
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters())
  • 损失是负对数似然,优化器是Adam优化器。
  • PyTorch中的负对数似然性需要对数概率,因此需要将模型最终层中log softmax的原始输出传递给它。

训练

对于训练,遍历DataLoader,每次通过模型一批。训练一定数量的时代或直到早期停止。

  • 在每批之后,计算损失(with criterion(output, targets)),然后根据模型参数计算损失的梯度loss.backward()。这使用自动微分和反向传播来计算梯度。
  • 在计算梯度后,调用optimizer.step()用梯度更新模型参数。这是在每个训练批次上完成的,因此正在实施随机梯度下降(或者更确切地说是具有称为Adam的动量的版本)。
  • 对于每个批次,还计算监控的准确性,并且在训练循环完成后,开始验证循环。这将用于进行早期停止。
  • 当许多时期的验证损失没有减少时,提前停止会停止训练。每次验证损失确实减少时,都会保存模型权重,以便以后加载最佳模型。
  • 提前停止是防止训练数据过度拟合的有效方法。如果继续训练,训练损失将继续减少,但验证损失将增加,因为模型开始记住训练数据。提前停止可以防止这种情况发生
  • 通过在每个训练时期结束时迭代验证数据并计算损失来实现早期停止。每次都使用完整的验证数据,并记录损失是否减少。如果它没有多个时代,停止训练,检索最佳权重,并返回它们。在验证循环中,确保不更新模型参数。

训练结果

可以通过观察来检查训练进度history。

正如预期的那样,训练损失随着时代的推移而不断下降。没有大量的过度拟合,可能是因为使用的是Dropout。由于损失的分歧,进一步训练所获得的收益并不多。

验证丢失显示由于验证图像数量较少而导致的异常行为

与损失一样,训练准确性增加,而验证准确性普遍存在。该模型能够立即达到约79%的准确度,表明在Imagenet上学习的卷积权重能够轻松转移到我们的数据集。

注意:这里验证数据集中只有9个每个类的图像

测试模型

在对模型进行训练以确定验证数据没有进一步改进之后,需要对它从未见过的数据进行测试。为了最终估计模型的性能,需要使用保持测试数据。在这里将查看单个预测以及整个测试数据集的损失和准确性。

代码语言:javascript
复制
def predict(image_path, model, topk=4):
    real_class = image_path.split('/')[-2]
 
    # Convert to pytorch tensor
    img_tensor = process_image(image_path)
 
    # Resize
    if train_on_gpu:
        img_tensor = img_tensor.view(1, 3, 224, 224).cuda()
 
    # Set to evaluation
    with torch.no_grad():
        model.eval()
        # Model outputs log probabilities
        out = model(img_tensor)
        ps = torch.exp(out)
 
        # Find the topk predictions
        topk, topclass = ps.topk(topk, dim=1)
 
        # Extract the actual classes and probabilities
        top_classes = [model.idx_to_class[class_] for class_ in topclass.cpu().numpy()[0]]
        top_p = topk.cpu().numpy()[0]
 
        return img_tensor.cpu().squeeze(), top_p, top_classes, real_class
代码语言:javascript
复制
img, top_p, top_classes, real_class = predict(random_test(), model)
top_p, top_classes, real_class
Output:
(array([0.5736144,0.26276276,0.15282246,0.01080029], dtype=float32), 
 ['DRUSEN', 'NORMAL', 'CNV', 'DME'],'DRUSEN')

模型调查

虽然该模型运行良好,但可能采取的步骤可以使其更好。通常弄清楚如何改进模型的最佳方法是调查其错误(注意:这也是一种有效的自我改进方法。)

看起来模型在测试集上运行良好。试着对它们有更多的直觉。

此功能显示图片以及topk模型中的预测。图像上的标题显示真正的类。

代码语言:javascript
复制
def predict(image_path, model, topk=4):
    real_class = image_path.split('/')[-2]
    img_tensor = process_image(image_path)
 
    # Resize
    if train_on_gpu:
        img_tensor = img_tensor.view(1, 3, 224, 224).cuda()
 
    # Set to evaluation
    with torch.no_grad():
        model.eval()
        # Model outputs log probabilities
        out = model(img_tensor)
        ps = torch.exp(out)
        topk, topclass = ps.topk(topk, dim=1)
 
        # Extract the actual classes and probabilities
        top_classes = [model.idx_to_class[class_] for class_ in topclass.cpu().numpy()[0]]
        top_p = topk.cpu().numpy()[0]
        return img_tensor.cpu().squeeze(), top_p, top_classes, real_class

这些都很简单,所以很高兴模型没有问题!

结论

  • 能够看到使用PyTorch的基础知识以及迁移学习的概念。
  • 以及如何在医学科学领域中使用这种图像传感技术

完整的代码可以在GitHub存储库中找到

https://github.com/pawangeek/pytorch-notebooks/tree/master/Retinal-image-classification

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 相约机器人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档