AI+医疗:使用神经网络进行医学影像识别分析 ⛵

虚幻大学 xuhss 209℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PXT9U78R-1660192624727)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3ade763a894430e8360d8a6067facd9~tplv-k3u1fbpfcp-zoom-1.image)]

? 作者:韩信子@ShowMeAI
? 计算机视觉实战系列https://www.showmeai.tech/tutorials/46
? 行业名企应用系列https://www.showmeai.tech/tutorials/63
? 本文地址https://www.showmeai.tech/article-detail/298
? 声明:版权所有,转载请联系平台与作者并注明出处
? 收藏ShowMeAI查看更多精彩内容

? 深度学习+医疗科技

近年高速发展的人工智能技术应用到了各个垂直领域,比如把深度学习应用于各种医学诊断,效果显著甚至在某些方面甚至超过了人类专家。典型的 CV 最新技术已经应用于阿尔茨海默病的分类、肺癌检测、视网膜疾病检测等医学成像任务中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iqHCqhi5-1660192624731)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/698dc8f34c2d4b688cfaf462fd4423e1~tplv-k3u1fbpfcp-zoom-1.image)]

? 图像分割

图像分割是将图像按照内容物切分为不同组的过程,它定位出了图像中的对象和边界。语义分割是像素级别的识别,我们在很多领域的典型应用,背后的技术支撑都是图像分割算法,比如:医学影像、无人驾驶可行驶区域检测、背景虚化等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6VCgY8ea-1660192624733)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/502b1c315b1f4cdfadbc1125908a1140~tplv-k3u1fbpfcp-zoom-1.image)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CTIpU4fR-1660192624734)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e6334aa79be4e6cb67dd41990e115e2~tplv-k3u1fbpfcp-zoom-1.image)]

本文涉及到的深度学习基础知识,及计算机视觉详细知识,推荐大家阅读ShowMeAI的教程专栏:

? 语义分割典型网络 U-Net

U-Net 是一种卷积网络架构,用于快速、精确地分割生物医学图像。

关于语义分割的各类算法原理及优缺点对比(包括U-Net),ShowMeAI 在过往文章 ? 深度学习与CV教程(14) | 图像分割 (FCN,SegNet,U-Net,PSPNet,DeepLab,RefineNet) 中有详细详解。

U-Net 的结构如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-49m3q8je-1660192624735)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cfefb97c186a4058adb8d2b8c64e6448~tplv-k3u1fbpfcp-zoom-1.image)]
在 U-Net 中,与其他所有卷积神经网络一样,它由卷积和最大池化等层次组成。

  • U-Net 简单地将编码器的特征图拼接至每个阶段解码器的上采样特征图,从而形成一个梯形结构。该网络非常类似于 Ladder Network 类型的架构。
  • 通过跳跃 拼接 连接的架构,在每个阶段都允许解码器学习在编码器池化中丢失的相关特征。
  • 上采样采用转置卷积。

? 使用 U-Net 进行肺部影像分割

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhwTzw0q-1660192624736)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43422fa607c544918eb10f1595b14a89~tplv-k3u1fbpfcp-zoom-1.image)]

我们这里使用到的数据集是 ? 蒙哥马利县 X 射线医学数据集。 该数据集由肺部的各种 X 射线图像以及每个 X 射线的左肺和右肺的分段图像的图像组成。大家也可以直接通过ShowMeAI的百度网盘链接下载此数据集。

? 实战数据集下载(百度网盘):公众号『ShowMeAI研究中心』回复『实战』,或者点击 这里 获取本文 [10] 使用神经网络进行肺部医学影像识别与分析masked montgomery county x-ray set 肺部医学影像数据集

ShowMeAI官方GitHubhttps://github.com/ShowMeAI-Hub

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j9JxEEIw-1660192624737)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2cbcfa6982f4435bb0ac6218a120d6b~tplv-k3u1fbpfcp-zoom-1.image)]

① 工具库导入&环境设置

首先导入我们本次使用到的工具库。


|  | # 导入工具库 |
|  | import os |
|  | import numpy as np |
|  | import cv2 |
|  | from glob import glob |
|  | from sklearn.model\_selection import train\_test\_split |
|  | import tensorflow as tf |
|  | from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau |
|  | from tensorflow.keras.optimizers import Adam |
|  | from tensorflow.keras.metrics import Recall, Precision |

② 数据读取

接下来我们完成数据读取部分,这里读取的内容包括图像和蒙版(mask,即和图片同样大小的标签)。我们会调整维度大小,以便可以作为 U-Net 的输入。


|  | # 读取X射线图像 |
|  | def imageread(path,width=512,height=512): |
|  |  x = cv2.imread(path, cv2.IMREAD\_COLOR) |
|  |  x = cv2.resize(x, (width, height)) |
|  |  x = x/255.0 |
|  |  x = x.astype(np.float32) |
|  | return x |
|  |  |
|  | # 读取标签蒙版 |
|  | def maskread(path\_l, path\_r,width=512,height=512): |
|  |  x\_l = cv2.imread(path\_l, cv2.IMREAD\_GRAYSCALE) |
|  |  x\_r = cv2.imread(path\_r, cv2.IMREAD\_GRAYSCALE) |
|  |  x = x\_l + x\_r |
|  |  x = cv2.resize(x, (width, height)) |
|  |  x = x/np.max(x) |
|  |  x = x > 0.5 |
|  |  x = x.astype(np.float32) |
|  |  x = np.expand\_dims(x, axis=-1) |
|  | return x |

③ 数据切分

我们要对模型的效果进行有效评估,所以接下来我们进行数据划分,我们把全部数据分为训练集、验证集和测试集。具体代码如下:


|  | """加载与切分数据""" |
|  | def load\_data(path, split=0.1): |
|  |  images = sorted(glob(os.path.join(path, "CXR\_png", "*.png"))) |
|  |  masks\_l = sorted(glob(os.path.join(path, "ManualMask", "leftMask", "*.png"))) |
|  |  masks\_r = sorted(glob(os.path.join(path, "ManualMask", "rightMask", "*.png"))) |
|  |  split\_size = int(len(images) * split) # 9:1的比例切分 |
|  |  train\_x, val\_x = train\_test\_split(images, test\_size=split\_size, random\_state=42) |
|  |  train\_y\_l, val\_y\_l = train\_test\_split(masks\_l, test\_size=split\_size, random\_state=42) |
|  |  train\_y\_r, val\_y\_r = train\_test\_split(masks\_r, test\_size=split\_size, random\_state=42) |
|  |  train\_x, test\_x = train\_test\_split(train\_x, test\_size=split\_size, random\_state=42) |
|  |  train\_y\_l, test\_y\_l = train\_test\_split(train\_y\_l, test\_size=split\_size, random\_state=42) |
|  |  train\_y\_r, test\_y\_r = train\_test\_split(train\_y\_r, test\_size=split\_size, random\_state=42) |
|  |  |
|  | return (train\_x, train\_y\_l, train\_y\_r), (val\_x, val\_y\_l, val\_y\_r), (test\_x, test\_y\_l, test\_y\_r) |

④ TensorFlow IO准备

我们会使用到 TensorFlow 进行训练和预估,我们用 TensorFlow 读取 numpy array 格式的数据,转为 TensorFlow 的 tensor 形式,并构建方便以 batch 形态读取和训练的 dataset 格式。


|  | # tensor格式转换 |
|  | def tf\_parse(x, y\_l, y\_r): |
|  | def \_parse(x, y\_l, y\_r): |
|  |  x = x.decode() |
|  |  y\_l = y\_l.decode() |
|  |  y\_r = y\_r.decode() |
|  |  x = imageread(x) |
|  |  y = maskread(y\_l, y\_r) |
|  | return x, y |
|  |  x, y = tf.numpy\_function(\_parse, [x, y\_l, y\_r], [tf.float32, tf.float32]) |
|  |  x.set\_shape([512, 512, 3]) |
|  |  y.set\_shape([512, 512, 1]) |
|  | return x, y |
|  |  |
|  | # 构建tensorflow dataset |
|  | def tf\_dataset(X, Y\_l, Y\_r, batch=8): |
|  |  dataset = tf.data.Dataset.from\_tensor\_slices((X, Y\_l, Y\_r)) |
|  |  dataset = dataset.shuffle(buffer\_size=200) |
|  |  dataset = dataset.map(tf\_parse) |
|  |  dataset = dataset.batch(batch) |
|  |  dataset = dataset.prefetch(4) |
|  | return dataset |

⑤ U-Net 网络构建

下面我们构建 U-Net 网络。


|  | from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Conv2DTranspose, Concatenate, Input |
|  | from tensorflow.keras.models import Model |
|  |  |
|  | # 一个卷积块结构 |
|  | def conv\_block(input, num\_filters): |
|  |  x = Conv2D(num\_filters, 3, padding="same")(input) |
|  |  x = BatchNormalization()(x) |
|  |  x = Activation("relu")(x) |
|  |  |
|  |  x = Conv2D(num\_filters, 3, padding="same")(x) |
|  |  x = BatchNormalization()(x) |
|  |  x = Activation("relu")(x) |
|  |  |
|  | return x |
|  |  |
|  | # 编码器模块 |
|  | def encoder\_block(input, num\_filters): |
|  |  x = conv\_block(input, num\_filters) |
|  |  p = MaxPool2D((2, 2))(x) |
|  | return x, p |
|  |  |
|  | # 解码器模块 |
|  | def decoder\_block(input, skip\_features, num\_filters): |
|  |  x = Conv2DTranspose(num\_filters, (2, 2), strides=2, padding="same")(input) |
|  |  x = Concatenate()([x, skip\_features]) |
|  |  x = conv\_block(x, num\_filters) |
|  | return x |
|  |  |
|  | # 完整的U-Net |
|  | def build\_unet(input\_shape): |
|  |  inputs = Input(input\_shape) |
|  |  |
|  | # 编码器部分 |
|  |  s1, p1 = encoder\_block(inputs, 64) |
|  |  s2, p2 = encoder\_block(p1, 128) |
|  |  s3, p3 = encoder\_block(p2, 256) |
|  |  s4, p4 = encoder\_block(p3, 512) |
|  |  |
|  |  b1 = conv\_block(p4, 1024) |
|  |  |
|  | # 解码器部分 |
|  |  d1 = decoder\_block(b1, s4, 512) |
|  |  d2 = decoder\_block(d1, s3, 256) |
|  |  d3 = decoder\_block(d2, s2, 128) |
|  |  d4 = decoder\_block(d3, s1, 64) |
|  |  |
|  | # 输出 |
|  |  outputs = Conv2D(1, 1, padding="same", activation="sigmoid")(d4) |
|  |  |
|  |  model = Model(inputs, outputs, name="U-Net") |
|  | return model |

⑥ 评估准则与损失函数

我们针对语义分割场景,编写评估准则 IoU 的计算方式,并构建 Dice Loss 损失函数以便在医疗场景语义分割下更针对性地训练学习。

关于IoU、mIoU等评估准则可以查看ShowMeAI的文章 ? 深度学习与CV教程(14) | 图像分割 (FCN,SegNet,U-Net,PSPNet,DeepLab,RefineNet) 做更多了解。

关于Dice Loss损失函数的解释如下:

? Dice 系数

根据 Lee Raymond Dice 命名,是一种集合相似度度量函数,通常用于计算两个样本的相似度(值范围为 [0,1][0, 1]):

s=2|X∩Y||X|+|Y|s = \frac{2|X \cap Y|}{|X|+|Y|}
|X∩Y||X \cap Y|表示 XX 和 YY 之间的交集;|X||X| 和 |Y||Y| 分别表示 XX 和 YY 的元素个数。其中,分子中的系数 22,是因为分母存在重复计算 XX 和 YY 之间的共同元素的原因。

针对,语义分割问题而言,XX 为分割图像标准答案 GT,YY 为分割图像预测标签 Pred。

? Dice 系数差异函数(Dice loss)

s=1−2|X∩Y||X|+|Y|s =1- \frac{2|X \cap Y|}{|X|+|Y|}
评估准则与损失函数的代码实现如下:


|  | # IoU计算 |
|  | def iou(y\_true, y\_pred): |
|  | def f(y\_true, y\_pred): |
|  |  intersection = (y\_true * y\_pred).sum() |
|  |  union = y\_true.sum() + y\_pred.sum() - intersection |
|  |  x = (intersection + 1e-15) / (union + 1e-15) |
|  |  x = x.astype(np.float32) |
|  | return x |
|  | return tf.numpy\_function(f, [y\_true, y\_pred], tf.float32) |
|  |  |
|  | # Dice Loss定义 |
|  | smooth = 1e-15 |
|  | def dice\_coef(y\_true, y\_pred): |
|  |  y\_true = tf.keras.layers.Flatten()(y\_true) |
|  |  y\_pred = tf.keras.layers.Flatten()(y\_pred) |
|  |  intersection = tf.reduce\_sum(y\_true * y\_pred) |
|  | return (2. * intersection + smooth) / (tf.reduce\_sum(y\_true) + tf.reduce\_sum(y\_pred) + smooth) |
|  |  |
|  | def dice\_loss(y\_true, y\_pred): |
|  | return 1.0 - dice\_coef(y\_true, y\_pred) |

⑦ 超参数设置与模型编译

接下来在开始模型训练之前,我们先敲定一些超参数,如下:

  • 批次大型 batch size = 2
  • 学习率 learning rate= 1e-5
  • 迭代轮次 epoch = 30

我们使用 Adam 优化器进行训练,使用的评估指标包括 Dice 系数、IoU、召回率和精度。


|  | # 超参数 |
|  | batch\_size = 2 |
|  | lr = 1e-5 |
|  | epochs = 30 |
|  | model\_path = "models/model.h5" |
|  |  |
|  | # 读取数据 |
|  | dataset\_path = './NLM-MontgomeryCXRSet/MontgomerySet' |
|  | (train\_x, train\_y\_l, train\_y\_r), (val\_x, val\_y\_l, val\_y\_r), (test\_x, test\_y\_l, test\_y\_r) = load\_data(dataset\_path) |
|  |  |
|  | # 训练集与验证集 |
|  | train\_dataset = tf\_dataset(train\_x, train\_y\_l, train\_y\_r, batch=batch\_size) |
|  | val\_dataset = tf\_dataset(val\_x, val\_y\_l, val\_y\_r, batch=batch\_size) |
|  |  |
|  | # 构建模型 |
|  | model = build\_unet((512, 512, 3)) |
|  | # 评估准则 |
|  | metrics = [dice\_coef, iou, Recall(), Precision()] |
|  | # 编译模型 |
|  | model.compile(loss=dice\_loss, optimizer=Adam(lr), metrics=metrics) |

可以使用model.summary查看模型结构信息与参数量:


|  | model . summary() |

结果如下图所示(部分内容截图,全部模型信息较长):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9uRIUkSj-1660192624738)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1979734ea7cb4ef590b696c38c16105d~tplv-k3u1fbpfcp-zoom-1.image)]

⑧ 回调函数&模型训练

我们在回调函数中设置模型存储相关设置,学习率调整策略等,之后在数据集上进行训练。


|  | # 回调函数 |
|  | callbacks = [ |
|  |  ModelCheckpoint(model\_path, verbose=1, save\_best\_only=True), |
|  |  ReduceLROnPlateau(monitor='val\_loss', factor=0.1, patience=5, min\_lr=1e-8, verbose=1) |
|  |  ] |
|  |  |
|  | # 模型训练 |
|  | history = model.fit( |
|  |  train\_dataset, |
|  |  epochs=epochs, |
|  |  validation\_data=val\_dataset, |
|  |  callbacks=callbacks |
|  |  ) |

训练部分中间信息如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GijYRSZC-1660192624739)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/279703e1fe1046118502c63faede1934~tplv-k3u1fbpfcp-zoom-1.image)]
在训练模型超过 30 个 epoch 后,保存的模型(验证损失为 0.10216)相关的评估指标结果如下:

  • dice coef:0.9148
  • iou:0.8441
  • recall:0.9865
  • precision:0.9781
  • val_loss:0.1022
  • val_dice_coef: 0.9002
  • val_iou:0.8198
  • val_recall:0.9629
  • val_precision:0.9577

⑨ 模型加载与新数据预估

我们可以把刚才保存好的模型重新加载入内存,并对没有见过的测试数据集进行预估,代码如下:


|  | # 重新载入模型 |
|  | from tensorflow.keras.utils import CustomObjectScope |
|  | with CustomObjectScope({'iou': iou, 'dice\_coef': dice\_coef, 'dice\_loss': dice\_loss}): |
|  |  model = tf.keras.models.load\_model("/content/model.h5") |
|  |  |
|  |  |
|  | # 测试集预估 |
|  | from tqdm import tqdm |
|  | import matplotlib.pyplot as plt |
|  | ct=0 |
|  |  |
|  | # 遍历测试集 |
|  | for x, y\_l, y\_r in tqdm(zip(test\_x, test\_y\_l, test\_y\_r), total=len(test\_x)): |
|  | """ Extracing the image name. """ |
|  |  image\_name = x.split("/")[-1] |
|  |  |
|  | # 读取测试图片集 |
|  |  ori\_x = cv2.imread(x, cv2.IMREAD\_COLOR) |
|  |  ori\_x = cv2.resize(ori\_x, (512, 512)) |
|  |  x = ori\_x/255.0 |
|  |  x = x.astype(np.float32) |
|  |  x = np.expand\_dims(x, axis=0) |
|  |  |
|  | # 读取标签信息 |
|  |  ori\_y\_l = cv2.imread(y\_l, cv2.IMREAD\_GRAYSCALE) |
|  |  ori\_y\_r = cv2.imread(y\_r, cv2.IMREAD\_GRAYSCALE) |
|  |  ori\_y = ori\_y\_l + ori\_y\_r |
|  |  ori\_y = cv2.resize(ori\_y, (512, 512)) |
|  |  ori\_y = np.expand\_dims(ori\_y, axis=-1) # (512, 512, 1) |
|  |  ori\_y = np.concatenate([ori\_y, ori\_y, ori\_y], axis=-1) # (512, 512, 3) |
|  |  |
|  | # 预估 |
|  |  y\_pred = model.predict(x)[0] > 0.5 |
|  |  y\_pred = y\_pred.astype(np.int32) |
|  | #plt.imshow(y\_pred) |
|  |  |
|  | # 存储预估结果mask |
|  |  save\_image\_path = "./"+str(ct)+".png" |
|  |  ct+=1 |
|  |  y\_pred = np.concatenate([y\_pred, y\_pred, y\_pred], axis=-1) |
|  |  sep\_line = np.ones((512, 10, 3)) * 255 |
|  |  cat\_image = np.concatenate([ori\_x, sep\_line, ori\_y, sep\_line, y\_pred*255], axis=1) |
|  |  cv2.imwrite(save\_image\_path, cat\_image) |

部分结果可视化:

下面为2个测试样本的原始图像、原始掩码(标准答案)和预测掩码的组合图像:

测试用例的输入图像(左侧)、原始掩码标签(中间)、预测掩码(右侧)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nfwpyzQz-1660192624741)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/911e1afb28e8427d88e24deb4bcbe75f~tplv-k3u1fbpfcp-zoom-1.image)]

参考资料

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1dw8A5ce-1660192624742)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9190f41b8de4af38c8a1a0c96f0513b~tplv-k3u1fbpfcp-zoom-1.image)]

转载请注明:xuhss » AI+医疗:使用神经网络进行医学影像识别分析 ⛵

喜欢 (0)

您必须 登录 才能发表评论!