Datawhale AI夏令营笔记分享
Part1 脑PET图像分析和疾病预测挑战赛Baseline学习与练习
Part2 卷积神经网络CNN(云服务平台和本地部署遇到了一些问题总结)
Part3 CNNbaseline成功运行并实现ResNet-18 ResNet-50 ResNet-34 模型的运行与参数比较,「albumentations」库应用于数据增强的方法拓展
文章目录
前言
学习内容介绍:
PET图像 全称为脑部正电子发射计算机断层显像(brain positron emission tomography PET),是反映脑部病变的基因、分子、代谢及功能状态的显像。PET图像能够全面探索AD的病理机制,找出每个AD阶段最适合的生物标志物,从而更好的评估疾病[1]。
例如:阿尔兹海默症病理——以通过反映葡萄糖代谢的显像剂(18-FDG),利用独特的符合成像技术,显示出AD病灶的分布及葡萄糖代谢的变化,直接反映出AD(阿尔兹海默症)病灶的特征部位及其此部位的代谢特征,并且可以定量确定敏感的界限值,达到能与正常人相鉴别并对AD进行分期的目的。利用机器学习和深度学习实现辅助医疗诊断的目的。多模态医学图像,如磁共振成像(MRI)和正电子发射断层扫描(PET),由于可以提供各种信息,已被广泛用于诊断阿尔茨海默病(AD)等脑部疾病。PET扫描可以比MRI更早地检测器官和组织中的细胞变化。与MRI不同,PET数据由于成本、辐射或其他限制而难以获取。
阿尔兹海默症研究现状
阿尔茨海默症(AD)是一种神经退行性疾病,可导致认知能力下降、日常生活恶化以及行为和心理变化。AD的分类是基于PET图像中脑组织变化的频率,使用k近邻(KNN)、支持向量机(SVM)、线性判别分析(LDA)和卷积神经网络(CNN)技术的组合。
近两年更多的研究是多模式融合的方式,通过融合来自不同模态图像的互补信息,使得合成模态比单独模态的输入图像传达更好的信息描述。Song等[2]从FDG-PET中提取对AD诊断至关重要的GM区域。GM-PET模式包含结构MRI信息和功能PET信息。**提出了一种3D多尺度CNN网络,该网络结合了来自不同尺度特征的信息,同时捕获上下文信息和位置信息。**为了防止过度拟合,我们使用以下策略设计了这两个网络:1)使用更少的卷积层;(2) 减少卷积层的信道数量;(3) 使用GAP和丢弃层来减少冗余信息。此外,所提出的AD诊断框架使用单个输入网络,而不是特征融合方法中使用的多输入网络,因为我们的图像融合方法将多模式图像扫描融合为单个合成图像。
一、分析赛题
题目:
研究基于脑PET图像的疾病预测,本次大赛提供了脑PET数据集作为脑PET图像检测数据库的训练样本,参赛者需根据提供的样本构建模型,对轻度认知障碍进行分析和预测。
数据集:
脑PET图像检测数据库,记录了老年人受试志愿者的脑PET影像资料,其中包括确诊为轻度认知障碍(MCI)患者的脑部影像数据和健康人(NC)的脑部影像数据。
被试者按医学诊断分为两类:
NC:健康
MCI:轻度认知障碍
将题目转化为图像二分类问题,Baseline代码利用了基础逻辑回归的模型,简单进行了特征提取和分类,精度不是很高,但有利于学习线上编程的操作流程,阅读了一些文献了解了CV方向的最新模型积累相关的开发经验。
二、PET图像预处理
随机打乱该数据集的顺序,使得被训练的数据尽可能呈现正态分布,尽可能避免训练不充分的情况。
PET图像为脑部的3D图像,可以切片处理并采用分水岭算法分割淀粉样蛋白区域,即感兴趣域ROI,提取纹理特征,如LBP,HOG等。
图像特征统计是指对图像中的像素或图像区域进行统计分析,以提取和描述图像的特征信息。
- 占比特征:通过统计图像中不同像素值或颜色通道的像素数量,计算其在整个图像中的比例或占比。
- 边缘特征:边缘是图像中像素值或颜色发生剧烈变化的区域,通常表示物体的边界或纹理的边界。
- 纹理特征:描述了图像中的纹理信息,反映了图像的细节和结构。
三、模型的训练与测试
1.逻辑回归模型(Logistic regression)
逻辑回归分析,是一种线性回归分析模型,属于机器学习中的监督学习。主要是用来解决二分类问题。通过给定的n组数据(训练集)来训练模型,并在训练结束后对给定的一组或多组数据(测试集)进行分类。
统计方法baseline中人工提取的特征是哪些?
主要有:
①非零像素的数量
②零像素的数量
③平均值
④标准差
⑤在列方向上平均值不为零的数量
⑥在行方向上平均值不为零的数量
⑦列方向上的最大平均值
⑧行方向上的最大平均值
2.卷积神经网络(ResNet-18 模型)
卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习模型,广泛用于图像识别、计算机视觉和模式识别任务中。CNN 在处理具有网格结构数据(如图像)时表现出色,它能够自动学习和提取图像中的特征,并在分类、定位和分割等任务中取得优秀的性能。
ResNet网络中的亮点:
- 超深的网络结构(突破1000层)
- 提出residual模块(残差结构)
- 使用Batch Normalization加速训练(丢弃dropout)
# 自定义数据集
import os, sys, glob, argparse
import pandas as pd
import numpy as np
from tqdm import tqdm
import cv2
from PIL import Image
from sklearn.model_selection import train_test_split, StratifiedKFold, KFold
import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = True
import torchvision.models as models
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data.dataset import Dataset
import nibabel as nib
from nibabel.viewers import OrthoSlicer3D
# glob.glob()函数,⽤于查找⽂件⽬录和⽂件,并将搜索到的结果返回到⼀个列表中
train_path = glob.glob('work/dataset/Train/*/*')
test_path = glob.glob(''work/dataset/Test/*')
#打乱训练集和测试集
np.random.shuffle(train_path)
np.random.shuffle(test_path)
DATA_CACHE = {}
class XunFeiDataset(Dataset):
#初始化函数接收路径
def __init__(self, img_path, transform=None):
self.img_path = img_path
if transform is not None:
self.transform = transform
else:
self.transform = None
#⽤于获取指定索引的图像和标签
def __getitem__(self, index):
if self.img_path[index] in DATA_CACHE:#缓存图片存在
img = DATA_CACHE[self.img_path[index]]
else:#缓存图片不存在
img = nib.load(self.img_path[index])
img = img.dataobj[:,:,:, 0]#获取图像数据,通道信息和时间序列信息
DATA_CACHE[self.img_path[index]] = img
# 随机选择一些通道
idx = np.random.choice(range(img.shape[-1]), 50)
img = img[:, :, idx]
img = img.astype(np.float32)
if self.transform is not None:
img = self.transform(image = img)['image']
#对图片维度进行转置
img = img.transpose([2,0,1])
return img,torch.from_numpy(np.array(int('NC' in self.img_path[index])))
def __len__(self):
return len(self.img_path)
import albumentations as A
# 对训练数据集进⾏数据预处理
train_loader = torch.utils.data.DataLoader(
XunFeiDataset(train_path[:-10],
A.Compose([
A.RandomRotate90(),#图片旋转90度
A.RandomCrop(120, 120),#裁剪图像为120*120
A.HorizontalFlip(p=0.5),#图像水平翻转
A.RandomContrast(p=0.5),#随机改变图片的对比度
A.RandomBrightnessContrast(p=0.5),#随即改变图片的亮度
])
), batch_size=2, shuffle=True, num_workers=1, pin_memory=False
)
#对验证数据进行预处理
val_loader = torch.utils.data.DataLoader(
XunFeiDataset(train_path[-10:],
A.Compose([
A.RandomCrop(120, 120),
])
), batch_size=2, shuffle=False, num_workers=1, pin_memory=False
)
#对测试数据进行预处理
test_loader = torch.utils.data.DataLoader(
XunFeiDataset(test_path,
A.Compose([
A.RandomCrop(128, 128),
A.HorizontalFlip(p=0.5),
A.RandomContrast(p=0.5),
])
), batch_size=2, shuffle=False, num_workers=1, pin_memory=False
)
# 自定义CNN模型
class SelfDefine_CNN(nn.Module):
def __init__(self):
super(XunFeiNet, self).__init__()
#创建 ResNet-18 模型,并赋值给 model 变量
model = models.resnet18(True)
model.conv1 = torch.nn.Conv2d(50, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
model.avgpool = nn.AdaptiveAvgPool2d(1)
model.fc = nn.Linear(512, 2)
self.resnet = model
def forward(self, img):
out = self.resnet(img)
return out
model = SelfDefine_CNN()
model = model.to('cuda')#第二次打卡卡在了cuda的部署失败,所以删掉cuda相关语句在CPU进行实验就可以得到submit2.csv
criterion = nn.CrossEntropyLoss().cuda()
optimizer = torch.optim.AdamW(model.parameters(), 0.001)
#ResNet34模型结构
from torch import nn
import torch as t
from torch.nn import functional as F
from torch.autograd import Variable as V
class ResidualBlock(nn.Module): # 定义ResidualBlock类 (11)
def __init__(self,inchannel,outchannel,stride=1,shortcut=None): # 初始化,自动执行 (12)
super(ResidualBlock, self).__init__() # 继承nn.Module (13)
self.left = nn.Sequential( # 左网络,构建Sequential,属于特殊的module,类似于forward前向传播函数,同样的方式调用执行 (14)(31)
nn.Conv2d(inchannel,outchannel,3,stride,1,bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True),
nn.Conv2d(outchannel,outchannel,3,1,1,bias=False),
nn.BatchNorm2d(outchannel)
)
self.right = shortcut # 右网络,也属于Sequential,见(8)步可知,并且充当残差和非残差的判断标志。 (15)
def forward(self,x): # ResidualBlock的前向传播函数 (29)
out = self.left(x) # # 和调用forward一样如此调用left这个Sequential(30)
if self.right is None: # 残差(ResidualBlock)(32)
residual = x #(33)
else: # 非残差(非ResidualBlock) (34)
residual = self.right(x) # (35)
out += residual # 结果相加 (36)
print(out.size()) # 检查每单元的输出的通道数 (37)
return F.relu(out) # 返回激活函数执行后的结果作为下个单元的输入 (38)
class ResNet(nn.Module): # 定义ResNet类,也就是构建残差网络结构 (2)
def __init__(self,numclasses=1000): # 创建实例时直接初始化 (3)
super(ResNet, self).__init__() # 表示ResNet继承nn.Module (4)
self.pre = nn.Sequential( # 构建Sequential,属于特殊的module,类似于forward前向传播函数,同样的方式调用执行 (5)(26)
nn.Conv2d(50,64,7,2,3,bias=False), # 卷积层,输入通道数为3,输出通道数为64,包含在Sequential的子module,层层按顺序自动执行
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(3,2,1)
)
self.layer1 = self.make_layer(64,128,4) # 输入通道数为64,输出为128,根据残差网络结构将一个非Residual Block加上多个Residual Block构造成一层layer(6)
self.layer2 = self.make_layer(128,256,4,stride=2) # 输入通道数为128,输出为256 (18,流程重复所以标注省略7-17过程)
self.layer3 = self.make_layer(256,256,6,stride=2) # 输入通道数为256,输出为256 (19,流程重复所以标注省略7-17过程)
self.layer4 = self.make_layer(256,512,3,stride=2) # 输入通道数为256,输出为512 (20,流程重复所以标注省略7-17过程)
self.fc = nn.Linear(8192,numclasses) # 全连接层,属于残差网络结构的最后一层,输入通道数为512,输出为numclasses (21)
def make_layer(self,inchannel,outchannel,block_num,stride=1): # 创建layer层,(block_num-1)表示此层中Residual Block的个数 (7)
shortcut = nn.Sequential( # 构建Sequential,属于特殊的module,类似于forward前向传播函数,同样的方式调用执行 (8)
nn.Conv2d(inchannel,outchannel,1,stride,bias=False),
nn.BatchNorm2d(outchannel)
)
layers = [] # 创建一个列表,将非Residual Block和多个Residual Block装进去 (9)
layers.append(ResidualBlock(inchannel,outchannel,stride,shortcut)) # 非残差也就是非Residual Block创建及入列表 (10)
for i in range(1,block_num):
layers.append(ResidualBlock(outchannel,outchannel)) # 残差也就是Residual Block创建及入列表 (16)
return nn.Sequential(*layers) # 通过nn.Sequential函数将列表通过非关键字参数的形式传入,并构成一个新的网络结构以Sequential形式构成,一个非Residual Block和多个Residual Block分别成为此Sequential的子module,层层按顺序自动执行,并且类似于forward前向传播函数,同样的方式调用执行 (17) (28)
def forward(self,x): # ResNet类的前向传播函数 (24)
x = self.pre(x) # 和调用forward一样如此调用pre这个Sequential(25)
x = self.layer1(x) # 和调用forward一样如此调用layer1这个Sequential(27)
x = self.layer2(x) # 和调用forward一样如此调用layer2这个Sequential(39,流程重复所以标注省略28-38过程)
x = self.layer3(x) # 和调用forward一样如此调用layer3这个Sequential(40,流程重复所以标注省略28-38过程)
x = self.layer4(x) # 和调用forward一样如此调用layer4这个Sequential(41,流程重复所以标注省略28-38过程)
x = F.avg_pool2d(x,1) # 平均池化 (42)
x = x.view(x.size(0),-1) # 设置返回结果的尺度 (43)
return self.fc(x) # 返回结果 (44)
model = ResNet() # 创建ResNet残差网络结构的模型的实例 (1)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), 0.001)
3.[albumentations]库用于数据增强
相比torch自带的图像处理方法,albumentations库函数有更多的对图像的预处理的办法约70种。在相同的对图像的处理下,相较其他处理方式**处理的速度更快。**可以结合pytorch和tensorflow一起使用。广泛用于工业,深度学习研究,机器学习竞赛和开源项目。
包括:像素级变换、空间级变换。基于打卡任务二,拓展使用随机大小的内核将运动模糊应用于输入图像、中值滤波、使用随机大小的内核模糊输入图像三种数据增强的拓展方法(精度上升了0.05),记录运行时间(基本没有变化)。
A.OneOf([
A.MotionBlur(p=0.2), # 使用随机大小的内核将运动模糊应用于输入图像。
A.MedianBlur(blur_limit=3, p=0.1), # 中值滤波
A.Blur(blur_limit=3, p=0.1), # 使用随机大小的内核模糊输入图像。
], p=0.2)
四、模型的评价指标
将提取的特征矩阵作为模型输入进行模型训练,通过测试集验证模型分类效果,使用F1_score作为模型的评价指标。本科毕业设计是做过这个课题的,当时采用的方法很简单的CNN和改进SVM进行了比较,得到了不错的分类效果,但由于阿尔兹海默症的病理很难更为精准的捕捉感兴趣域的纹理特征,所以误判率会比较高。这次夏令营也想采用目前流行的一些CV模型解决这个问题,类似STA-CNN,ResNet等。
总结
随着模型的调整不断更新结果:
**1.第一次打卡:**结果分数为0.48485,后续改进特征提取方法和训练模型后再进行打分
**2.第二次打卡:**结果分数暂时提交了决策树的分类效果,树的深度为4时,精度为0.37778,变差了。调试CNN遇到了部署Pytorch失败的问题,本地由于之前的版本缺少很多包在打卡时没来得及完成。8.23日将CNN_baseline调试通过,最终结果分数为0.6938。ResNet-18的效果较传统机器学习模型精度提高了很多。
**3.第三次打卡:**结果分数为0.7421,ResNet-34相较于ResNet-18分类效果有所提高。ResNet-34_1优化之后效果最好达到0.7763.
- [albumentations]扩充三种方法之后的精度较CNNBaseline
2.精度为
| ResNet-18 | ResNet-34 | ResNet-50 |
|—0.69388–|—0.77632—|---0.2609–|
参考文献
[1]Wang, J., Jin, C., Zhou, J. et al. PET molecular imaging for pathophysiological visualization in Alzheimer’s disease. Eur J Nucl Med Mol Imaging 50, 765–783 (2023). https://doi.org/10.1007/s00259-022-05999-z
[2]Song J , Zheng J , Li P ,et al.An Effective Multimodal Image Fusion Method Using MRI and PET for Alzheimer’s Disease Diagnosis[J].Frontiers in Digital Health, 2021, 3:637386.DOI:10.3389/fdgth.2021.637386.