休息的意思机器学习⼊门之:不需要带脑⼦,全⽹最细,详解Unet代码
⽂章⽬录
Unet
为什么 Unet 适合做医学影像处理
通信培训图像语义较为简单、结构较为固定。做脑的,就⽤脑CT和脑MRI,做胸⽚的只⽤胸⽚CT,做眼底的只⽤眼底OCT,都是⼀个固定的器官的成像,⽽不是全⾝的。由于器官本⾝结构固定和语义信息没有特别丰富,所以⾼级语义信息和低级特征都显得很重要(UNet的skip connection和U型结构就派上了⽤场)。
数据量少。医学影像的数据获取相对难⼀些,很多⽐赛只提供不到100例数据。所以我们设计的模型不宜过⼤,参数过多,很容易导致过拟合。
原始UNet的参数量在28M左右(上采样带转置卷积的UNet参数量在31M左右),⽽如果把channel数成倍缩⼩,模型可以更⼩。缩⼩两倍后,UNet参数量在7.75M。缩⼩四倍,可以把模型参数量缩⼩⾄2M以内,⾮常轻量。
多模态。相⽐⾃然影像,医疗影像⽐较有趣和不同的⼀点是,医疗影像是具有多种模态的。以ISLES脑
梗竞赛为例,其官⽅提供了CBF,MTT,CBV,TMAX,CTP等多种模态的数据。这就需要我们更好的设计⽹络去提取不同模态的特征feature。这⾥提供两篇论⽂供⼤家参考。
Joint Sequence Learning and Cross-Modality Convolution for 3D Biomedical Segmentation(CVPR 2017) ,
Den Multi-path U-Net for Ischemic Stroke Lesion Segmentation in Multiple Image Modalities.
可解释性重要。由于医疗影像最终是辅助医⽣的临床诊断,所以⽹络告诉医⽣⼀个3D的CT有没有病是远远不够的,医⽣还要进⼀步的想知道,病灶在哪⼀层,在哪⼀层的哪个位置,分割出来了吗,能求体积嘛?同时对于⽹络给出的分类和分割等结果,医⽣还想知道为什么,所以⼀些神经⽹络可解释性的trick就有⽤处了,⽐较常⽤的就是画activation map。看⽹络的哪些区域被激活了
参考⽂章:
Unet 结构展⽰
复现的代码
这是基于 Keras 结构写的,通俗易懂
1. 主函数 main
from model import*
from data import*
#os.environ["CUDA_VISIBLE_DEVICES"] = "0"
'''通过 dict 函数把这些参数都变成字典中的项'''
data_gen_args =dict(rotation_range=0.2,# 旋转范围
国庆节升旗仪式
width_shift_range=0.05,# 宽度变换范围
height_shift_range=0.05,# ⾼度变换范围
shear_range=0.05,# 剪切范围
zoom_range=0.05,# 变焦范围
horizontal_flip=True,# ⽔平翻转
fill_mode='nearest')# 填充模式(近邻填充)
myGene = trainGenerator(2,'data/membrane/train','image','label',data_gen_args,save_to_dir =None)#产⽣训练数据(以⽣成器的⽅式对数据集做增⼴)model = unet()
model_checkpoint = ModelCheckpoint('unet_membrane.hdf5', monitor='loss',verbo=1, save_best_only=True)# 提前设置保存模型的⼀些参数model.fit_generator(myGene,steps_per_epoch=300,epochs=1,callbacks=[model_checkpoint])# 需要设置 steps_per_epoch来适应 fit_generator testGene = testGenerator("data/membrane/test")# 产⽣测试数据
results = model.predict_generator(testGene,30,verbo=1)# 对于模型输⼊⼀个⽣成器
saveResult("data/membrane/test",results)
2. data.py
让我们看看这个代码⾥是如何定义 trainGenerator的
neytiri
def trainGenerator(batch_size,train_path,image_folder,mask_folder,aug_dict,image_color_mode ="grayscale",
mask_color_mode ="grayscale",image_save_prefix ="image",mask_save_prefix ="mask",
flag_multi_class =Fal,num_class =2,save_to_dir =None,target_size =(256,256),ed =1):
'''
can generate image and mask at the same time
u the same ed for image_datagen and mask_datagen to ensure the transformation for image and mask is the same if you want to visualize the results of generator, t save_to_dir = "your path"
'''
image_datagen = ImageDataGenerator(**aug_dict)
rolls roycemask_datagen = ImageDataGenerator(**aug_dict)
sounds是什么意思
image_generator = image_datagen.flow_from_directory(# 将图⽚的 batch 分好
train_path,
class =[image_folder],
class_mode =None,公斤的英文
color_mode = image_color_mode,
target_size = target_size,
batch_size = batch_size,
fullscalesave_to_dir = save_to_dir,
save_prefix = image_save_prefix,
ed = ed)
mask_generator = mask_datagen.flow_from_directory(# 将图⽚的 mask 都分好
train_path,
class =[mask_folder],
class_mode =None,
color_mode = mask_color_mode,
target_size = target_size,
batch_size = batch_size,
save_to_dir = save_to_dir,
save_prefix = mask_save_prefix,
ed = ed)
# for i in range(5):
# print(())
# print(().shape) # shape = (batch_size, 256, 256, 1)
train_generator =zip(image_generator, mask_generator)
for(img,mask)in train_generator:
img,mask = adjustData(img,mask,flag_multi_class,num_class)
yield(img,mask)
image_datagen = ImageDataGenerator(**aug_dict) 根据 main 函数中的 data_gen_args 中规定的这些参数来进⾏数据集的扩展,把已有的数据进⾏:
同样的⽅式对 label 数据也以同样的⽅式进⾏扩展 mask_datagen = ImageDataGenerator(**aug_dict)
扩展完数据集和标签集之后,要将这些数据进⾏ batch 的划分;这个步骤使⽤了flow_from_directory,
flow_from_directory(): 以⽂件夹路径(directory)为参数,将经过数据提升/归⼀化后的数据(⽂中的 image_datagen 和
mask_datagen),在⼀个⽆限循环中⽆限产⽣batch数据;
具体的⽤法可以参考这篇博客:
接下来,如果⼤家有兴趣,可以打印⼀下 image_generator 和 mask_generator 中的数据的维度,你会发现,他们产⽣的都是 4 维的数据;也就是说,从这两个⽣成器中出来的每⼀个变量的维度都是 4,记住这⼀点,后⾯要⽤
所以这个时候 image_generator 和 mask_generator 产⽣的数据 img 和 mask 中的数据维度就是 (2, 256, 256, 1) 四个维度分别代表了batchsize、targetsize、图⽚的通道数,由于图⽚都是灰度图,所以最后⼀个通道数为 1;也就是说:
每次 image_generator 和 mask_generator 运⾏⼀次,都会从增强和扩展后的数据集中拿出 2 张 image 图⽚ 和 对应的 2 张 mask 标签图⽚;这些图⽚都是 (256,256) 的维度,并且都是单通道的灰度图
按照正常的思路⼀步步来看,接下来由于在 trainGenerator 模块中涉及到了 adjustData 这个函数,所以我们再来看⼀下这个函数做了什么⼯作:
从输⼊的参数上来看,除了上⼀部分提到的打包好的训练数据 img 和 训练标签 mask,还有 flag_multi_class 以及 num_class flag_multi_class 是个多类型检测的标志,如果 True 那么证明⼀个图中有多个种类的分类⽬标
num_class 是告诉函数,⼀共分⼏类
def adjustData(img,mask,flag_multi_class,num_class):
if(flag_multi_class):# 如果⼀个场景⾥有多个识别的物体
img = img /255# 图⽚特征缩放成0-1之间
mask = mask[:,:,:,0]if(len(mask.shape)==4)el mask[:,:,0]# 取mask颜⾊的值
new_mask = np.zeros(mask.shape +(num_class,))# 变成 5 维的矩阵,(batch_size,255,255,1,num_class)
笨人晚宴for i in range(num_class):
#for one pixel in the image, find the class in mask and convert it into one-hot vector
#index = np.where(mask == i)
日语等级#index_mask = (index[0],index[1],index[2],np.zeros(len(index[0]),dtype = np.int64) + i) if (len(mask.shape) == 4) el (index[0],index[1],np.zeros(len (index[0]),dtype = np.int64) + i)
#new_mask[index_mask] = 1
new_mask[mask == i,i]=1# 在 mask == i 的位置,这些值全部变成1,然后作为 new_mask 这样,new_mask就是个⿊⽩的图像了
if flag_multi_class:
new_mask = np.reshape(new_mask,(new_mask.shape[0],new_mask.shape[1]*new_mask.shape[2],new_mask.shape[3])) el:
new_mask = np.reshape(new_mask,(new_mask.shape[0]*new_mask.shape[1],new_mask.shape[2]))
mask = new_mask
elif(np.max(img)>1):
img = img /255
mask = mask /255
mask[mask >0.5]=1
mask[mask <=0.5]=0
return(img,mask)
通过 adjustData 的代码,根据默认的 flag_multi_class = Fal 所以我们关注的应该是代码的 elif 后的部分,即:
先判断如果整个 img (2, 256, 256, 1)变量中所有的像素点中的最⼤值 > 1,那就证明整个 img 变量中的图⽚都是还未进⾏归⼀化;这个时候使⽤ /255. 的⽅式可以把所有像素点的范围规范到 0 ~ 1上,把特征进⾏压缩;
mask[mask > 0.5] = 1 就是遍历 mask 中所有的值,> 0.5 的值被设置为 1,<0.5 的被设置成 0 ;其实就是对标签的图⽚进⾏了⼆值化的处理。
到这⾥adjustData 的部分其实就算解析完了;但是,为了防⽌⼩伙伴们对于 flag_multi_class 的部分有疑问,我还是觉得应该解析⼀下:
对于 img 来说没有什么特殊,还是先把所有的像素点 /255.
对于 mask;由于 flag_multi_class = True, 这个时候代表需要分类的种类变多了,这个时候 num_class 参数的作⽤就可以发挥出来;
我们先⼀⾏⾏来看
不管怎么样都是取最后⼀维的像素点值;那么这样的话数据的维度也会减⼩ 1 维,从 (2, 256, 256, 1)-->(2, 256, 256)
然后创建⼀个全零的矩阵,矩阵的维度是 (2, 256, 256) + (num_class, ) = (2, 256, 256, num_class) 结合应⽤的场景来看, num_class ⼀般为 2,所以全零矩阵的维度应该为 (2, 256, 256, 2),然后