肺部感染识别

1. 数据集简介

数据集目录展示:

数据集大小:

NORMALPNEUMONIAtotal
train134138755216
val8816
test234390624

样本展示:

下载地址:

https://www.kaggle.com/paultimothymooney/chest-xray-pneumonia/download

2. 主要流程

1. 加载预训练模型ResNet-50,该模型已在ImageNet上训练过;

2. 冻结预训练模型中低阶卷积层的参数(权重);

3. 用可训练参数的多层替换分类层;

4. 在训练集上训练分类层;

5. 微调超参数,根据需要解冻更多层。

ResNet网络结构图

备注:

18=(2+2+2+2)*2+2=8*2+2

50=(3+4+6+3)*3+2=16*3+2

152=(3+8+36+3)*3+2=50*3+2

跳连机制,防止梯度消失 !!!

3. 数据探索

3.1 数据增强与加载

1. 加载库

import os
import torch
import torch.nn as nn
from torch import optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
from torchvision.utils import make_grid
import numpy as np
import matplotlib.pyplot as plt

2. 加载数据集(数据存储在本地)

def load_data(data_path, batch_size):
    # 定义图片格式转换器
    data_transforms = {}
    # 加载数据集train和val,并进行格式转换
    image_datasets = {
        x: datasets.ImageFolder(os.path.join(data_path, x), transform=data_transforms[x])
        for x in ['train', 'val', 'test']
    }

    # 为数据集创建一个迭代器,读取数据
    dataloaders = {
        x: DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True, pin_memory=True)
        for x in ['train', 'val', 'test']
    }
    return dataloaders

3. 图片转换,数据增强

data_transforms = {
    'train': transforms.Compose([
        # 随机长宽比裁剪原始数据,crop后比例在0.8-1.1倍之间
        transforms.RandomResizedCrop(300, scale=(0.8, 1.1)),
        transforms.RandomRotation(degrees=10),  # 随机旋转一定角度(-10度 到 10度之间)
        transforms.ColorJitter(0.4, 0.4, 0.4),  # 修改亮度、对比度、饱和度
        transforms.RandomHorizontalFlip(),  # 水平翻转
        transforms.CenterCrop(size=256),  # 根据指定的size从中心裁剪
        transforms.ToTensor(),  # numpy --> tensor
        # 对数据按通道进行标准化(RGB), (x-mean)/std
        transforms.Normalize([0.485, 0.456, 0.406],  # mean
                             [0.229, 0.224, 0.225])  # std
    ]),
    'val': transforms.Compose([
        transforms.Resize(300),
        transforms.CenterCrop(256),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], 
                             [0.229, 0.224, 0.225])
    ]),
    'test': transforms.Compose([
        transforms.RandomResizedCrop(300),
        transforms.CenterCrop(256),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225])
    ])
}

3.2 显示数据集中的图片

参考:https://blog.csdn.net/MusicDancing/article/details/122107467

3.2.1 绘制图片

从url读取图片

from PIL import Image

def image_show_3(img):
    plt.figure(figsize=(8, 8))
    plt.imshow(img)
    plt.axis('off')   # 关闭坐标轴
    plt.show()

img = Image.open(data_loader['train'].dataset.root + '/NORMAL/IM-0117-0001.jpeg')
image_show_3(img)

3.2.2 图片显示

方法1、2显示一组图片

方法1显示一张图片

3.3 显示数据集信息

1. 获取Label

# 获取标签的类别名称
target_names = ['NORMAL', 'PNEUMONIA']
target_names = image_datasets['train'].classes
data_loader['train'].dataset.classes
# ['NORMAL', 'PNEUMONIA']
    
print(image_datasets['train'].class_to_idx)
# {'NORMAL': 0, 'PNEUMONIA': 1}
    
print(image_datasets['train'].class_to_idx.items())
# dict_items([('NORMAL', 0), ('PNEUMONIA', 1)])
 
# 反转字典键与值   
LABEL = dict((v, k) for k, v in image_datasets['train'].class_to_idx.items())
print(LABEL)
# {0: 'NORMAL', 1: 'PNEUMONIA'}

2. 训练集信息显示

data_loader['train'].dataset

显示一张图片的tensor和label

print(data_loader['train'].dataset[1])

3. 常用指标获取

# 获取训练集样本数
len(data_loader['train'].dataset)
# 5216

# 训练集的路径
data_loader['train'].dataset.root
# ../data/chest_xray/train

# 训练集正常图片目录下的所有文件
files_normal = os.listdir(os.path.join(str(data_loader['train'].dataset.root), 'NORMAL'))

4. 模型训练Pipeline

4.1 迁移学习,模型微调

Transfer Learning 就是把已经训练好的模型参数迁移到新的模型来帮助新模型训练。

1. 通过迁移学习,拿到一个成熟的模型,进行模型微调。

# 获取预训练模型,并替换池化层和全连接层
def get_model():
    model = models.resnet50(pretrained=True)
    # 1. 冻结预训练模型中的所有参数
    for param in model.parameters():
        param.requires_grad = False
    # 2. 微调模型:替换ResNet最后的两层网络,返回一个新的模型
    model.avgpool = AdaptiveConcatPool2d()  # 池化层替换
    model.fc = nn.Sequential(
        nn.Flatten(),           # 所有维度拉平

        nn.BatchNorm1d(4096),   # 正则化处理,加速神经网络收敛过程,提高训练过程中的稳定性,256*8*2=4096
        nn.Dropout(0.5),        # 丢掉一些神经元,0.5 被验证是最好的
        nn.Linear(4096, 512),   # 线性层的处理
        nn.ReLU(),

        nn.BatchNorm1d(512),
        nn.Dropout(0.5),
        nn.Linear(512, 2),      # 2个输出
        nn.LogSoftmax(dim=1)    # 损失函数:将input转换成概率分布的形式,输出2个概率
    )
    return model

2. 自定义自适应连接池化层

class AdaptiveConcatPool2d(nn.Module):
    def __init__(self, size=None):
        super(AdaptiveConcatPool2d, self).__init__()
        # 池化层的卷积核kernel大小,当且仅当size为0或None或False时返回(1, 1)
        size = size or (1, 1)  
        # 自适应算法能够自动计算核的大小和每次移动的步长
        self.avgPooling = nn.AdaptiveAvgPool2d(size)  # 池化层1
        self.maxPooling = nn.AdaptiveMaxPool2d(size)  # 池化层2

    def forward(self, x):
        # 按列拼接
        return torch.cat((self.maxPooling(x), self.avgPooling(x)), dim=1)

4.2 模型训练

4.2.1 训练函数主流程

# 返回一个训练效果最好的模型,并保存
def train(model, device, dataloaders, criterion, optimizer, epochs, model_path):
    print('{0:>20} | {1:>20} | {2:>20} | {3:>20} |'.format('Epoch', 'Training loss', 'Val loss', 'Val acc'))
    # 用无穷大初始化训练损失
    best_loss = np.inf
    # 循环读取数据进行训练和验证
    for epoch in range(epochs):
        train_loss = train_epoch(model, device, dataloaders['train'], criterion, optimizer)
        val_loss, val_acc = val_epoch(model, device, dataloaders['val'], criterion)
        print('{0:>20} | {1:>20} | {2:>20} | {3:>20.2f} |'.format(epoch, train_loss, val_loss, val_acc))
        # 保存损失函数最小时对应的模型结构
        if val_loss < best_loss:
            best_loss = val_loss
            # 保存模型训练过程中学习到的权重和偏置系数
            torch.save(model.state_dict(), model_path)

4.2.2 定义单轮训练函数

def train_epoch(model, device, train_loader, criterion, optimizer):
    model.train()
    total_loss = 0.0   # 总损失
    # 循环读取训练数据集,更新模型参数
    for batch_id, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()      # 梯度初始化为0
        output = model(data)    # 模型输出
        loss = criterion(output, target)   # 计算一个batch的损失,带有梯度
        # tensor(0.9706, grad_fn= < NllLossBackward0 >)
        print(loss)
        loss.backward()     # 反向传播
        optimizer.step()    # 更新参数
        # 累计训练损失,item()获取标量,data.size(0)为batch大小
        total_loss += loss.item() * data.size(0) 
 
    total_loss /= len(train_loader.dataset)   # 平均损失
    return total_loss

4.2.3 定义单轮验证函数

def val_epoch(model, device, val_loader, criterion):
    model.eval()
    total_loss = 0.0
    total_acc = 0.0
    with torch.no_grad():
        for data, target in val_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)  # 前向传播输出
            # item()输出标量,这里不需要梯度反向传播
            total_loss += criterion(output, target).item() * images.size(0)

            # 获取预测结果中每行数据概率最大的下标
            _, pred = torch.max(output, dim=1)
            # 累计预测正确的个数
            total_acc += torch.sum(pred == target).item()

            # 打印错误分类的图片信息
            misclassified_images(pred, target, data, epoch)

    total_loss /= len(val_loader.dataset)
    total_acc /= len(val_loader.dataset)
    return total_loss, total_acc

误分类图片追踪显示

# 记录误分类的图片, 默认显示10个
def misclassified_images(pred, target, data, epoch, count=10):
    LABEL = {0: 'NORMAL', 1: 'PNEUMONIA'}
    # 找出预测值与真实label不一致的样本
    misclassified = (pred != target)
    for index, image_tensor in enumerate(data[misclassified][:count]):
        img_name = 'Epoch:{} --> Predict:{} --> Actual:{}'\
            .format(epoch,
                LABEL[pred[misclassified].tolist()[index]],
                LABEL[target[misclassified].tolist()[index]])
        print(img_name, image_tensor)

Epoch:0 --> Predict:NORMAL --> Actual:PNEUMONIA 0
Epoch:0 --> Predict:NORMAL --> Actual:PNEUMONIA 0
Epoch:0 --> Predict:NORMAL --> Actual:PNEUMONIA 0
Epoch:0 --> Predict:NORMAL --> Actual:PNEUMONIA 0
Epoch:0 --> Predict:NORMAL --> Actual:PNEUMONIA 0

6. 主函数:定义超参数,构建pipeline

def main():
    BATCH_SIZE = 8  # 128
    EPOCHS = 10
    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    DATA_PATH = '../data/chest_xray'
    MODEL_PATH = '../model/res50_new.model'

    model = get_model().to(DEVICE)
    criterion = nn.NLLLoss()   # 损失函数,一般用于多分类
    # optimizer = optim.Adam(model.parameters())
    optimizer = optim.SGD(model.parameters(), lr=0.001)

    data_loader = load_data(DATA_PATH, BATCH_SIZE)
    train(model, DEVICE, data_loader, criterion, optimizer, EPOCHS, MODEL_PATH)
    eval(model, data_loader['test'])


if __name__ == "__main__":
    main()

5. 模型评估

5.1 计算正确率

import torch
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
from mlxtend.plotting import plot_confusion_matrix

def cal_accuracy(outputs, labels):
    _, preds = torch.max(outputs, dim=1)
    acc = torch.tensor(torch.sum(preds == labels).item() / len(preds))
    return acc

5.2 统计混淆矩阵、P、R、F1

def metrics(outputs, labels):
    _, preds = torch.max(outputs, dim=1)
    cm = confusion_matrix(labels.cpu().numpy(), preds.cpu().numpy())
    plot_confusion(cm)
    tn, fp, fn, tp = cm.ravel()
    precision = tp / (tp + fp)
    recall = tp / (tp + fn)
    f1 = 2 * ((precision * recall) / (precision + recall))
    return precision, recall, f1

5.3 绘制混淆矩阵

def plot_confusion(cm):
    plt.figure()
    plot_confusion_matrix(cm, figsize=(8, 4), cmap=plt.cm.Blues)
    plt.xticks(range(2), ['Normal', 'Pneumonia'], fontsize=14)
    plt.yticks(range(2), ['Normal', 'Pneumonia'], fontsize=14)
    plt.xlabel('Predicted Label', fontsize=16)
    plt.ylabel('True Label', fontsize=16)
    plt.show()

11

5.4 测试集评估,指标统计

def eval(model, test_loader):
    precision_list = []
    recall_list = []
    f1_list = []
    accuracy_list = []

    model.eval()
    with torch.no_grad():
        # 由于测试集是分批读取的,所以需把每个batch的结果保存在list中
        for datas, labels in test_loader:
            outputs = model(datas)
            precision, recall, f1 = metrics(outputs, labels)
            acc = cal_accuracy(outputs, labels)
            precision_list.append(precision)
            recall_list.append(recall)
            f1_list.append(f1)
            accuracy_list.append(acc.item())

    p = sum(precision_list) / len(precision_list)
    r = sum(recall_list) / len(recall_list)
    f1 = sum(f1_list) / len(f1_list)
    acc = sum(accuracy_list) / len(accuracy_list)
    print('P: {0}  R: {1}  F1: {2}  Acc:{3}'.format(p, r, f1, acc))  

5.5 调用及结果展示

eval(model, data_loader['test'])

6. 整体框架Review

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值