小组成员:潘序(组长),梁禹,万立志,方宇豪
青光眼,作为一种导致不可逆视力丧失的眼科疾病,在全球范围内对人类健康构成了重大威胁。
据估计,到2020年,全球将有超过1100万人因青光眼而失明。鉴于其早期症状不明显,早期诊断对于预防视力损害至关重要。
然而,专业眼科医生的缺乏,尤其是在偏远地区,限制了青光眼的早期筛查和治疗。
为了解决这一问题,我们小组设计了一种基于卷积神经网络(CNN)的青光眼眼底病变检测模型——EyeNet。
数据集链接:Fundus Glaucoma Detection Data [PyTorch format]
Github:EyeNet: A Convolutional Neural Network for Glaucomatous Fundus Lesion Detection
测试当前环境是否可用GPU训练。
import torch
print(torch.__version__) # 查看torch当前版本号
print(torch.version.cuda) # 编译当前版本的torch使用的cuda版本号
print(torch.cuda.is_available()) # 查看当前cuda是否可用于当前版本的Torch,如果输出True,则表示可用
2.3.0+cu118 11.8 True
在EyeNet中,卷积层由多个Conv2d层组成,负责提取输入图像的特征。
第一个卷积层将输入通道数从3(RGB图像)增加到64,使用3x3的卷积核;
第二个卷积层将通道数从64增加到128;
第三个卷积层保持通道数为128;
第四个卷积层将通道数从128增加到256;
第五个卷积层保持通道数为256。
每个卷积层后面都紧跟一个ReLU激活函数,用于引入非线性。
EyeNet中的池化层用于降低特征图的空间维度,以减少计算量并增加感受野,同时也提高了模型对小的位置变化的不变性。
在EyeNet中,池化层使用了2x2的池化核和2的步长,使得输出为输入大小的1/4。
除了最后一个卷积层,ReLU激活函数在EyeNet的每个卷积层后面使用,这是因为ReLU能够在训练过程中提供快速的收敛速度,并且减轻梯度消失的问题。
import torch
import torch.nn as nn
class EyeNet(nn.Module):
def __init__(self):
super(EyeNet,self).__init__()
self.Conv = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(64,128, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2 ,stride=2),
nn.Conv2d(128,128, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(128,256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2,stride = 2),
nn.Conv2d(256,256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(2,2)
)
self.classify = nn.Sequential(
nn.Linear(25*25*256,512),
nn.ReLU(),
torch.nn.Linear(512,128),
nn.ReLU(),
nn.Dropout(p = 0.5),
nn.Linear(128,1)
)
def forward(self,x):
x = self.Conv(x)
x = x.view(-1, 25 * 25 * 256)
x = self.classify(x)
return x
打印模型结构:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
md = EyeNet().to(device)
print(md)
EyeNet( (Conv): Sequential( (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): ReLU() (2): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): ReLU() (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (5): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (6): ReLU() (7): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (8): ReLU() (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (11): ReLU() (12): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (classify): Sequential( (0): Linear(in_features=160000, out_features=512, bias=True) (1): ReLU() (2): Linear(in_features=512, out_features=128, bias=True) (3): ReLU() (4): Dropout(p=0.5, inplace=False) (5): Linear(in_features=128, out_features=1, bias=True) ) )
本模块包含了一个最基本的PyTorch的数据集加载器和一个getROI(image)函数。
import torch
import cv2
import numpy as np
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import os
经过小组查阅资料,青光眼病变时医生通常根据眼底图像,一般是后视网膜图像中的眼底视盘区域的形状改变进行诊断。
这块区域在图像中呈现的是一个明显的偏亮的圆形或椭圆形区域。
因此,我们使用getROI函数,找到图像中平均像素值最高的一个200x200区域并返回。
这是一种数据增强的方式,我们小组通过这种方法大大缩短了神经网络的训练时间,并且能够防止模型的过拟合。
getROI()函数示意图如下:
def getROI(image):
if isinstance(image, Image.Image):
image = np.array(image)
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred_image = cv2.GaussianBlur(gray_image, (65, 65), 0)
max_intensity_pixel = np.unravel_index(np.argmax(blurred_image), gray_image.shape)
radius = 200 // 2
x = max_intensity_pixel[1] - radius
y = max_intensity_pixel[0] - radius
x = max(0, x)
y = max(0, y)
square_size = 2 * radius
mask = np.zeros((gray_image.shape[0], gray_image.shape[1], 3), dtype=np.uint8)
cv2.rectangle(mask, (x, y), (x + square_size, y + square_size), (255, 255, 255), -1)
roi_image = cv2.bitwise_and(image, mask)
cropped_roi = roi_image[y:y+square_size, x:x+square_size]
resized_roi = cv2.resize(cropped_roi, (square_size, square_size))
return resized_roi
根据数据集的摆放方式创建图像-标签表供模型训练和评估。PyTorch规定了这一类的编写方法。
class GlaucomaDataset(Dataset):
def __init__(self, data_dir, transform=None):
self.data_dir = data_dir
self.transform = transform
# 获取所有图像的路径和标签
self.images = []
self.labels = []
for label in [0, 1]:
label_dir = os.path.join(self.data_dir, str(label))
for img_file in os.listdir(label_dir):
if img_file.lower().endswith(('.png', '.jpg', '.jpeg')):
self.images.append(os.path.join(label_dir, img_file))
self.labels.append(label)
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
# 加载图像
image = Image.open(self.images[idx]).convert('RGB') # 确保图像是RGB格式
# 应用自定义的getROI函数
image = getROI(image)
# 应用转换操作
if self.transform:
image = self.transform(image)
# 标签
label = torch.tensor(self.labels[idx], dtype=torch.long)
return image, label
1、学习率:首先设置为0.01,发现模型loss几乎不下降,于是逐步降低至0.0001发现模型loss下降,且在测试集的正确率上升。最后训练到模型准确率到95%左右时发现loss和accuracy都停滞,于是继续减小学习率,直至模型历史正确率高达98.56%。
2、损失函数:由于我们拟解决的是一个二分类问题,于是我们使用了二元交叉熵损失函数。
3、优化器:我们使用了Adam优化器,来优化反向传播时参数调整的过程。
前期工作结果:
训练至第150 epoch的模型(准确率98.56%),其历史准确率折线图如下:
在第30epoch训练完毕后,发现准确度不上升,调整学习率后发现准确率高速上升。
import torch
import os
import torch.nn as nn
from torchvision import transforms
import torch.optim as optim
from models import model
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
from utils.dataLoader import GlaucomaDataset
import predict
# 数据集路径
data_dir = 'datas/archive'
learning_rate = 0.00001 #学习率在训练过程中更改
transform = transforms.Compose([
transforms.ToTensor(), # 转换为PyTorch张量
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
]) #归一化操作
train_dataset = GlaucomaDataset(os.path.join(data_dir, 'train'), transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
md = model.EyeNet().cuda()
criterion = nn.BCEWithLogitsLoss().cuda()
optimizer = optim.Adam(md.parameters(),lr = learning_rate)
首先看train函数输入,如果不是从0 epoch开始训练的则使用预训练的权重。然后使用数据集加载器加载的图像-标签对,对模型进行训练。具体的训练过程依次包括梯度归零、前向传播计算输出、损失函数计算、反向传播和优化器优化。
每一个epoch训练完毕后保存一次模型,并且调用下一个Cell中的eva()函数,使用本epoch的模型对测试集图像标签对进行准确率评估。如果本epoch训练的结果模型准确度创下整个训练过程的历史记录,则将本模型保存为best model。
def train(pre_epochs, epochs):
losses = []
accuracies = []
max_accuracy = 0
# 如果预训练过了就加载已有权重
if pre_epochs >= 1:
md.load_state_dict(torch.load('check_point.pth'))
for epoch in range(pre_epochs, epochs):
print("epoch=",epoch)
for i,(features,label) in enumerate(train_loader):
optimizer.zero_grad()
outputs = md(features.cuda())
outputs = outputs.squeeze()
loss = criterion(outputs, label.float().cuda())
loss.backward()
optimizer.step()
if (i+1) % 10 == 0:
print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch, epochs, i+1, len(train_loader), loss.item()))
losses.append(loss.item())
torch.save(md.state_dict(), 'check_point.pth') #每一个epoch训练结束保存一次模型
acc = predict.eva()
accuracies.append(acc)
if acc > max_accuracy:
max_accuracy = acc
torch.save(md.state_dict(), f'best_model.pth')
plt.plot(losses)
plt.xlabel("ITERATIONS")
plt.ylabel("Loss")
plt.title("Training Loss Curve")
plt.show()
# plt.plot(accuracies)
# plt.xlabel("ITERATIONS")
# plt.ylabel("Accuracy")
# plt.title("Accuracy Curve")
# plt.show()
if __name__ == '__main__':
train(150,151) #跑一个epoch作为示例
epoch= 150 Epoch [150/151], Step [10/270], Loss: 0.0883 Epoch [150/151], Step [20/270], Loss: 0.0182 Epoch [150/151], Step [30/270], Loss: 0.0522 Epoch [150/151], Step [40/270], Loss: 0.0222 Epoch [150/151], Step [50/270], Loss: 0.0146 Epoch [150/151], Step [60/270], Loss: 0.0017 Epoch [150/151], Step [70/270], Loss: 0.0131 Epoch [150/151], Step [80/270], Loss: 0.0235 Epoch [150/151], Step [90/270], Loss: 0.0111 Epoch [150/151], Step [100/270], Loss: 0.0020 Epoch [150/151], Step [110/270], Loss: 0.0213 Epoch [150/151], Step [120/270], Loss: 0.0128 Epoch [150/151], Step [130/270], Loss: 0.0399 Epoch [150/151], Step [140/270], Loss: 0.0221 Epoch [150/151], Step [150/270], Loss: 0.0149 Epoch [150/151], Step [160/270], Loss: 0.0090 Epoch [150/151], Step [170/270], Loss: 0.0322 Epoch [150/151], Step [180/270], Loss: 0.0039 Epoch [150/151], Step [190/270], Loss: 0.0372 Epoch [150/151], Step [200/270], Loss: 0.0097 Epoch [150/151], Step [210/270], Loss: 0.0623 Epoch [150/151], Step [220/270], Loss: 0.0017 Epoch [150/151], Step [230/270], Loss: 0.0039 Epoch [150/151], Step [240/270], Loss: 0.0390 Epoch [150/151], Step [250/270], Loss: 0.0069 Epoch [150/151], Step [260/270], Loss: 0.0117 Epoch [150/151], Step [270/270], Loss: 0.0070 Accuracy of the network on the test images: 98.85 %
本模式只能由train(pre_epochs, epochs)函数调用,不可由使用者直接调用。
如果想对单张图片进行青光眼诊断,则可以使用下一Cell的diagnose()函数。
def eva():
md = model.EyeNet().cuda()
md.load_state_dict(torch.load("check_point.pth"))
md.eval()
transform = transforms.Compose([
transforms.ToTensor() # Convert to PyTorch tensor
])
data_dir = 'datas/archive'
test_dataset = GlaucomaDataset(os.path.join(data_dir, 'test'), transform)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
close_count = 0
total = 0
tolerance = 0.5
with torch.no_grad():
for features, label in test_loader:
output = md(features)
output = output.squeeze() # Ensure output is a 1D tensor for comparison
total += label.size(0)
difference = torch.abs(output - label.squeeze()) # Make sure label is also 1D for comparison
close_count += (difference <= tolerance).sum().item() # Correctly count predictions within tolerance
if total > 0:
close_ratio = close_count / total
print('Percentage of predictions within 0.2 of true value: {:.2f}%'.format(close_ratio*100.0))
return close_ratio
else:
print('No samples to evaluate.')
可以通过本模式,对指定的图像,使用我们的最高准确率模型进行青光眼诊断。
本Cell中使用了数据集中archive/val中的后视网膜图像,并且取出其中8张进行诊断结果可视化。
import torch
from PIL import Image
import numpy as np
from torchvision import transforms
from models import model
from utils import dataLoader
import matplotlib.pyplot as plt
import os
def diagnosis(path, md, transform):
md.eval() # 模型处于评估模式(不训练)
img = Image.open(path).convert('RGB') # 确保图片是RGB格式
img = dataLoader.getROI(img)
img = transform(img) # 应用转换操作
img = torch.unsqueeze(img, 0)
img = img.cuda()
with torch.no_grad():
output = md(img)
output = output.squeeze()
predicted_prob = torch.sigmoid(output)
# 根据概率确定诊断结果
diagnosis_result = 'Normal' if predicted_prob.item() < 0.5 else 'Glaucoma'
return diagnosis_result
def visualize_results(results):
fig, axes = plt.subplots(2, 4, figsize=(20, 10))
for i, (img_path, result, true_label) in enumerate(results):
img = Image.open(img_path).convert('RGB')
ax = axes[i // 4, i % 4]
ax.imshow(img)
ax.axis('off')
ax.set_title(f"model: {result}\nreal: {'Glaucoma' if true_label == 1 else 'Normal'}")
plt.tight_layout()
plt.show()
def main():
# 实例化模型并加载权重
md = model.EyeNet().cuda()
md.load_state_dict(torch.load("best_model.pth"))
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
val_dir = 'datas/archive/val'
glaucoma_images = [os.path.join(val_dir, '1', img) for img in os.listdir(os.path.join(val_dir, '1'))[:4]]
normal_images = [os.path.join(val_dir, '0', img) for img in os.listdir(os.path.join(val_dir, '0'))[:4]]
results = []
for img_path in glaucoma_images:
result = diagnosis(img_path, md, transform)
results.append((img_path, result, 1)) # 1表示真实标签为青光眼阳性
for img_path in normal_images:
result = diagnosis(img_path, md, transform)
results.append((img_path, result, 0)) # 0表示真实标签为青光眼阴性
visualize_results(results)
# 运行主程序
if __name__ == "__main__":
main()