作者 | Pawan Jain
来源 | Medium
编辑 | 代码医生团队
初学者的教程,在OCT视网膜图像上的pytorch中使用vgg16架构实现迁移学习。
深度学习有可能通过对人类专家进行难以分类并快速检查大量图像来彻底改变疾病诊断和管理。
关于数据集
视网膜OCT图像的该数据集是从Kaggle数据集获得的。有四类图像CNV,DME,DRUSEN和NORMAL
https://www.kaggle.com/paultimothymooney/kermany2018
关于OCT
光学相干断层扫描(OCT)是一种使用相干光捕获生物组织的高分辨率图像的成像技术。
探索数据集
试着看看每个类别中的图像数量和图像的大小。
for d in os.listdir(traindir):
categories.append(d)
# Number of each image
train_imgs = os.listdir(traindir + d)
valid_imgs = os.listdir(validdir + d)
test_imgs = os.listdir(testdir + d)
n_train.append(len(train_imgs))
n_valid.append(len(valid_imgs))
n_test.append(len(test_imgs))
# Dataframe of categories
cat_df = pd.DataFrame({'category': categories,'n_train': n_train,
'n_valid': n_valid, 'n_test': n_test})
cat_df.sort_values('n_train', ascending=False, inplace=True)
cat_df.head()
得到一个数据框,其中包含测试,训练和验证文件夹中每个类别的图像计数,通过它可以获得关于数据集的一些基本直觉
图像预处理
要为网络准备图像,必须将它们调整为224 x 224,并通过减去平均值并除以标准偏差来标准化每个颜色通道。还将在此阶段增加训练数据。这些操作是使用图像完成的,图像transforms为神经网络准备数据。
for i in train_imgs:
img_categories.append(d)
img = Image.open(traindir + d + '/' + i)
img_array = np.array(img)
# Shape
hs.append(img_array.shape[0])
ws.append(img_array.shape[1])
sns.kdeplot(img_dsc['height']['mean'], label='Average Height')
sns.kdeplot(img_dsc['width']['mean'], label='Average Width')
这有助于可视化训练集中图像的宽度和高度范围
当在预先训练的网络中使用图像时,必须将它们重塑为224 x 224.这是图像的大小,因此是模型所期望的。大于此的图像将被截断,而较小的图像将被插值。
数据扩充
由于图像数量有限,可以使用图像增强来人为地增加网络“看到”的图像数量。这意味着,对于训练,会随机调整大小并裁剪图像,并将其水平翻转。对每个时期应用不同的随机变换(在训练时),因此网络有效地看到同一图像的许多不同版本。
image_transforms = {
# Train uses data augmentation
'train':
transforms.Compose([
transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)),
transforms.RandomRotation(degrees=15),
transforms.ColorJitter(), #randomly change brightness,hue and saturaion of images
transforms.RandomHorizontalFlip(),transforms.CenterCrop(size=224),
transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225])]),
# Validation and test does not use augmentation
'val':
transforms.Compose([
transforms.Resize(size=256),transforms.CenterCrop(size=224),
transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
'test':
transforms.Compose([
transforms.Resize(size=256),transforms.CenterCrop(size=224),
transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
}
数据迭代器
为避免一次将所有数据加载到内存中,使用训练DataLoaders。
一个关键方面是shuffle将数据传递给网络之前的数据。这意味着图像类别的排序在每次通过数据时都会发生变化(一次通过数据是一个训练时期)。
data = {'train':datasets.ImageFolder(root=traindir, transform=image_transforms['train']),
'val':datasets.ImageFolder(root=validdir, transform=image_transforms['val']),
'test':datasets.ImageFolder(root=testdir, transform=image_transforms['test'])}
# Dataloader iterators
dataloaders = {'train': DataLoader(data['train'], batch_size=batch_size, shuffle=True),
'val': DataLoader(data['val'], batch_size=batch_size, shuffle=True),
'test': DataLoader(data['test'], batch_size=batch_size, shuffle=True)}
使用预先训练的模型(VGG-16)
预训练背后的想法CNN是与许多图像识别任务相关的提取特征的早期卷积层。后面的完全连接的图层通过学习更高级别的特征来专门处理特定数据集。
因此可以使用已经训练过的卷积层,同时只训练自己的数据集上的完全连接的层。事实证明,经过预先训练的网络可以在各种任务中取得相当的成功,并且可以显着缩短训练时间,并且通常可以提高性能。
VGG-16架构
这classifier是将训练的模型的一部分。然而对于vgg,只需要在分类器中训练最后几层,甚至不需要训练所有完全连接的层。
vgg16的分类器部分,具有1000个out_features
已经跳过了上面部分的代码,可以在Github存储库或Kaggle内核中查看它们。
https://github.com/pawangeek/pytorch-notebooks/tree/master/Retinal-image-classification
https://www.kaggle.com/kernels/scriptcontent/15436113/download
otal_params = sum(p.numel() for p in model.parameters())
print(f'{total_params:,} total parameters.')
total_trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'{total_trainable_params:,} training parameters.')
共有135,310,404个参数。
1,049,860个训练参数。
将类映射到索引
为了跟踪模型所做的预测,创建了类到索引和索引到类的映射。这将知道给定预测的实际类。
model.class_to_idx = data['train'].class_to_idx
model.idx_to_class = {idx: class_
for class_, idx in model.class_to_idx.items()}
list(model.idx_to_class.items())
[(0,'CNV'),(1,'DME'),(2,'DRUSEN'),(3,'NORMAL')]
所以完整模型是经过预先训练的vgg + custom classifier。这是一个很长的模型,不可能在这里发布。不过设法通过使用来自上一个自定义模块的快照torchsummary
模型的自定义分类器
训练损失和优化
损失(标准):跟踪损失本身以及相对于模型参数(重量)的损失梯度
优化器:使用渐变更新参数(重量)
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters())
训练
对于训练,遍历DataLoader,每次通过模型一批。训练一定数量的时代或直到早期停止。
训练结果
可以通过观察来检查训练进度history。
正如预期的那样,训练损失随着时代的推移而不断下降。没有大量的过度拟合,可能是因为使用的是Dropout。由于损失的分歧,进一步训练所获得的收益并不多。
验证丢失显示由于验证图像数量较少而导致的异常行为
与损失一样,训练准确性增加,而验证准确性普遍存在。该模型能够立即达到约79%的准确度,表明在Imagenet上学习的卷积权重能够轻松转移到我们的数据集。
注意:这里验证数据集中只有9个每个类的图像
测试模型
在对模型进行训练以确定验证数据没有进一步改进之后,需要对它从未见过的数据进行测试。为了最终估计模型的性能,需要使用保持测试数据。在这里将查看单个预测以及整个测试数据集的损失和准确性。
def predict(image_path, model, topk=4):
real_class = image_path.split('/')[-2]
# Convert to pytorch tensor
img_tensor = process_image(image_path)
# Resize
if train_on_gpu:
img_tensor = img_tensor.view(1, 3, 224, 224).cuda()
# Set to evaluation
with torch.no_grad():
model.eval()
# Model outputs log probabilities
out = model(img_tensor)
ps = torch.exp(out)
# Find the topk predictions
topk, topclass = ps.topk(topk, dim=1)
# Extract the actual classes and probabilities
top_classes = [model.idx_to_class[class_] for class_ in topclass.cpu().numpy()[0]]
top_p = topk.cpu().numpy()[0]
return img_tensor.cpu().squeeze(), top_p, top_classes, real_class
img, top_p, top_classes, real_class = predict(random_test(), model)
top_p, top_classes, real_class
Output:
(array([0.5736144,0.26276276,0.15282246,0.01080029], dtype=float32),
['DRUSEN', 'NORMAL', 'CNV', 'DME'],'DRUSEN')
模型调查
虽然该模型运行良好,但可能采取的步骤可以使其更好。通常弄清楚如何改进模型的最佳方法是调查其错误(注意:这也是一种有效的自我改进方法。)
看起来模型在测试集上运行良好。试着对它们有更多的直觉。
此功能显示图片以及topk模型中的预测。图像上的标题显示真正的类。
def predict(image_path, model, topk=4):
real_class = image_path.split('/')[-2]
img_tensor = process_image(image_path)
# Resize
if train_on_gpu:
img_tensor = img_tensor.view(1, 3, 224, 224).cuda()
# Set to evaluation
with torch.no_grad():
model.eval()
# Model outputs log probabilities
out = model(img_tensor)
ps = torch.exp(out)
topk, topclass = ps.topk(topk, dim=1)
# Extract the actual classes and probabilities
top_classes = [model.idx_to_class[class_] for class_ in topclass.cpu().numpy()[0]]
top_p = topk.cpu().numpy()[0]
return img_tensor.cpu().squeeze(), top_p, top_classes, real_class
这些都很简单,所以很高兴模型没有问题!
结论
完整的代码可以在GitHub存储库中找到
https://github.com/pawangeek/pytorch-notebooks/tree/master/Retinal-image-classification