[!] 该任务是本人在大一下学期初完成,专业知识尚未成熟
了解肺脏与肺部疾病类型
X射线检测和计算机断层扫描的原理
了解COVID-19疾病
掌握卷积神经网络概念及基础结构
掌握卷积神经网络在X光片检测患者肺炎系统的应用
使用卷积神经网络搭建X光片检测患者肺炎系统
注:以下可能含有引起不适的内容
严重并发症包含急性呼吸窘迫综合征(ARDS)、脓毒症休克、全身炎症反应综合征、难以纠正的代谢性酸中毒、急性心肌损伤、凝血功能障碍,甚至死亡等。
疾病潜伏期通常约在暴露后4-5天左右,一般认为不会超过14天。97.5%的患者会在感染后11.5天内出现症状。目前认为无症状患者也具有传播疾病的能力。
卷积神经网络由一个或多个卷积层和顶端的全连通层(对应经典的神经网络)组成,同时也包括关联权重和池化层(pooling layer)
ReLU即线性整流层 f ( x ) = m a x ( 0 , x ) f(x)=max{(0,x)} f(x)=max(0,x),与Sigmoid等激活函数相比,ReLU能够有效地避免梯度断裂情况。同时,ReLU激活函数模拟了生物学中的神经元兴奋行为,能够将影响因素放大,提高神经网络的训练速度
图中红色线为ReLU激活函数 f ( x ) = m a x ( 0 , x ) f(x)=max(0,x) f(x)=max(0,x),蓝色线为Sigmoid激活函数 f ( x ) = 1 1 + e − x f(x)=\frac{1}{1+e^{-x}} f(x)=1+e−x1
X光片检测患者肺炎情况本质为经典的图像多分类任务,基于任务描述及知识储备,我们以蒙特利尔大学博士后 Joseph Cohen 所整理的X光片图像数据集作为病患数据源,以及来源于Kaggle的胸部健康X射线图像数据集作为健康数据源
本次任务所需模块如下:
import tensorflow as tf
from tensorflow.keras import Sequential,layers
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
import pandas as pd
import glob,os,random,pathlib
数据集由两个数据集组成,且Kaggle胸部健康X射线图像数据集具有少许噪声,以下为两个数据集内容对比。
系统的文件储存路径为
#加载数据集1
data1 = pd.read_csv('./data/metadata.csv',encoding='gb18030')
#查看第一张图片
temp = mpimg.imread('./data/images/' +data1.filename[0])
plt.imshow(temp)
plt.show()
通过Python字典来将标签转换为索引
#finding和filename为csv文件中的标签
labelToIndex = dict((name,number) for number,name in enumerate(list(set(data1.finding))))
"""
{'Chlamydophila': 0,
'Pneumocystis': 1,
'Streptococcus': 2,
'No Finding': 3,
'Klebsiella': 4,
'SARS': 5,
'COVID-19': 6,
'COVID-19, ARDS': 7,
'ARDS': 8,
'E.Coli': 9,
'Legionella': 10}
"""
随后使用Python两个平行列表来分别储存图像的位置和对应的标签
#打包路径和标签
dataPaths1 = ['./data/images/' + path for path in data1.filename]
dataLabels1 = [labeltoIndex[name] for name in data1.finding]
根据csv中可知前1300个样本皆为健康样本,故可以按顺序抽取其中若干份进行处理,且能看出因为Joseph Cohen并不包含健康情况,因此需要额外增加标签
#加载数据集2
data2 = pd.read_csv('./data/seMetadata.csv',encoding='gb18030')
#打包路径和标签
SIZE = 150
SIZE = min(1300,SIZE)
unormalType = len(list(set(data1.finding))) #不正常的种类
labelToIndex['Normal'] = UNORMAL #增加正常数据集
dataPaths2 = ['./data/seImages/' + path for path in data2.X_ray_image_name[:SIZE]]
dataLabels2 = [unormalType] * SIZE
随后,只需要把两组一一对应的列表相加即可得到完整的源数据集
#整合两个数据集
dataPaths = dataPaths1 + dataPaths2
dataLabels = dataLabels1 + dataLabels2
由于两份数据集接按顺序进行排列,若不进行打乱处理,会造成严重的过拟合现象
#打乱数据集
seed = random.randint(1,100)
random.seed(seed)
random.shuffle(dataPaths)
random.seed(seed)
random.shuffle(dataLabels)
#抽取前10个查看效果
print(dataPaths[:10],dataLabels[:10])
输出如下:
['./data/images/FE9F9A5D-2830-46F9-851B-1FF4534959BE.jpeg',
'./data/images/1.CXRCTThoraximagesofCOVID-19fromSingapore.pdf-002-fig3a.png',
'./data/images/covid-19-pneumonia-20.jpg',
'./data/images/covid-19-pneumonia-14-PA.png',
'./data/seImages/IM-0216-0001.jpeg',
'./data/images/streptococcus-pneumoniae-pneumonia-temporal-evolution-1-day2.jpg',
'./data/images/A7E260CE-8A00-4C5F-A7F5-27336527A981.jpeg',
'./data/seImages/IM-0207-0001.jpeg',
'./data/seImages/IM-0137-0001.jpeg',
'./data/seImages/IM-0154-0001.jpeg']
[6, 6, 6, 6, 11, 2, 6, 11, 11, 11]
经检查后确认无误
在Tensorflow中支持用户将数据整合为Dataset,并且可以直接使用Dataset类在keras中进行训练
#将数据集按照3:1:1的比例进行分类
SPILT = 0.6
trainData = tf.data.Dataset.from_tensor_slices((dataPaths[:int(SPILT*len(dataLabels))],dataLabels[:int(SPILT*len(dataLabels))]))
testData = tf.data.Dataset.from_tensor_slices((dataPaths[int(SPILT*len(dataLabels)):],dataLabels[int(SPILT*len(dataLabels)):]))
随后构建函数对图像进行预处理
def prePicPNG(path,label):
"""预处理图像"""
temp = tf.io.read_file(path)
temp = tf.cond(tf.image.is_jpeg(temp),
lambda: tf.image.decode_jpeg(temp, channels=3),
lambda: tf.image.decode_png(temp, channels=3))
temp = tf.cast(temp,tf.float32)
temp /= 255.0
temp = tf.image.resize(temp,[192,192])
return temp,label
使用map()遍历Dataset即可,同时对数据集进行打乱和打包
SHUFFLE = 1000
BATCH = 4
trainData = trainData.map(prePicPNG).shuffle(SHUFFLE)
trainData = trainData.batch(BATCH)
testData = testData.map(prePicPNG).shuffle(SHUFFLE).batch(1)
vaildData = vaildData.map(prePicPNG).shuffle(SHUFFLE).batch(1)
至此,数据已经准备完毕,可以进入到使用阶段
鉴于任务较为简单,因此使用模仿LeNet-5的经典网络模型来预测
#构建模型
model = Sequential()
model.add(layers.Conv2D(32,(5,5),padding = 'SAME',activation='relu',input_shape=[192,192,3]))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64,(3,32),padding = 'SAME',activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dense(12,activation='softmax'))
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.summary()
输出结果为:
将训练集和测试集载入到模型中,配置好基本的超参数即可开始训练
#训练模型并保存历史记录
history = model.fit(trainData,shuffle=True,epochs=10,validation_data=testData)
至此已经完成了模型的训练
使用matplotlib和vaildData来完成分析
#将数据展示
plt.plot(history.history['accuracy'],label = 'accuracy')
plt.plot(history.history['val_accuracy'],label = 'val_accuracy')
plt.plot(history.history['loss'],label = 'loss')
plt.plot(history.history['val_loss'],label = 'val_loss')
plt.legend(loc='best')
plt.show()
#预测验证集
model.predict(vaildData)
能够看出虽然有83.48%的准确率,损失值也有失控的趋势
为了能够让卷积神经网络预测图片,因此需要对图片进行预处理
def editInput(path):
"""解码处理图片"""
temp = tf.io.read_file(path)
temp = tf.image.decode_jpeg(temp,channels = 3) #RGB图对应3个特征图
temp = tf.image.resize(temp,[192,192])
return tf.reshape(temp,(1,192,192,3))
将功能封装起来,方便后续的使用
#索引-标签列表
indexName = [name for name,val in labelToIndex.items()]
def predictPic(path):
"""预测一个X光片的病例"""
test = editInput(path)
ans = model.predict(test).tolist()[0]
ans = ans.index(max(ans))
return indexName[ans]
对网络结构做了略微的修改,增加了Inception模块以及Dropout层
def Inception(x,n=1):
"""Inception模块"""
Inception1 = layers.Conv2D(n * 2,(1,1),padding='SAME',activation = 'relu')(x)
Inception1 = layers.Conv2D(n * 8,(1,3),padding='SAME',activation = 'relu')(Inception1)
Inception1 = layers.Conv2D(n * 8,(3,1),padding='SAME',activation = 'relu')(Inception1)
Inception1 = layers.Conv2D(n * 8,(1,3),padding='SAME',activation = 'relu')(Inception1)
Inception1 = layers.Conv2D(n * 8,(3,1),padding='SAME',activation = 'relu')(Inception1)
Inception2 = layers.Conv2D(n * 16,(1,1),padding='SAME',activation = 'relu')(x)
Inception3 = layers.Conv2D(n * 4,(1,1),padding='SAME',activation = 'relu')(x)
Inception3 = layers.Conv2D(n * 32,(1,3),padding='SAME',activation = 'relu')(Inception3)
Inception3 = layers.Conv2D(n * 32,(3,1),padding='SAME',activation = 'relu')(Inception3)
Inception4 = layers.MaxPooling2D((3,3),strides=(1, 1),padding = 'SAME')(x)
Inception4 = layers.Conv2D(n * 4,(1,1),padding='SAME',activation = 'relu')(Inception4)
return layers.Concatenate()([Inception1,Inception2,Inception3,Inception4])
整体结构代码如下:
inputs = layers.Input(shape=(192,192,3))
x = Inception(x)
x = layers.MaxPooling2D((2,2),strides=(2,2),padding = 'SAME')(x)
x = Inception(x,2)
x = layers.MaxPooling2D((2,2),strides=(2,2),padding = 'SAME')(x)
x = Inception(x,2)
x = layers.MaxPooling2D((2,2),strides=(2,2),padding = 'SAME')(x)
x = Inception(x,3)
x = layers.MaxPooling2D((2,2),strides=(2,2),padding = 'SAME')(x)
x = Inception(x,3)
x = layers.MaxPooling2D((2,2),strides=(2,2),padding = 'SAME')(x)
x = Inception(x,4)
x = layers.MaxPooling2D((2,2),strides=(2,2),padding = 'SAME')(x)
x = layers.Flatten()(x)
x = layers.Dense(1024,activation = 'relu')(x)
x = layers.Dense(512,activation = 'relu')(x)
x = tf.keras.layers.Dropout(0.5)(x)
outputs = layers.Dense(12,activation='softmax')(x)
model = Model(inputs=inputs,outputs=outputs)
训练效果如下:
能够看出有效缓解了过拟合情况,准确率维持在85%附近,并且测试集与测试集准确率和损失值保持在相同水平,预计为样本数目不足导致的准确度无法进一步提高
在验证集中能看出损失值降到了一个相对较低的水平,准确率也与预期相同
使用Callbacks中对验证集损失值进行监视,并且回收损失值最小的模型
callback = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3,restore_best_weights=True)
history = model.fit(trainData,shuffle=True,epochs=12,validation_data=testData,steps_per_epoch=120,callbacks=[callback])
能够看出数据集中样本数非常少,因此可以调整Kaggle中加入的健康数据集数目,来增加数据集的数目
训练效果如下:
能够看出准确率提高并稳定在90%左右,loss值进一步下降
使用验证集进行验证:
能够看出与分析内容大抵相同