Datawhale第三期CV方向任务二学习


前言

任务二主要是学习理解Pytorch CNN和使用paddle paddle套件进行模型训练,相较于任务一的baseline是有困难的,而且很多代码我都有点云里雾里的感觉,于是对着示例代码一行一行去查,有很多收获,就在这里记录一下。


一、Pytorch CNN

大佬手撕CNN讲解录屏
卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习模型,广泛用于图像识别、计算机视觉和模式识别任务中。CNN 在处理具有网格结构数据(如图像)时表现出色,它能够自动学习和提取图像中的特征,并在分类、定位和分割等任务中取得优秀的性能。

1.自定义数据集

import os, sys, glob, argparse
import pandas as pd
import numpy as np
from tqdm import tqdm

import cv2
from PIL import Image
from sklearn.model_selection import train_test_split, StratifiedKFold, KFold

import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = True

import torchvision.models as models
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data.dataset import Dataset

import nibabel as nib
from nibabel.viewers import OrthoSlicer3D

# 获取训练和测试数据的文件路径
train_path = glob.glob('./脑PET图像分析和疾病预测挑战赛公开数据/Train/*/*')
test_path = glob.glob('./脑PET图像分析和疾病预测挑战赛公开数据/Test/*')

# 随机打乱训练和测试数据的顺序
np.random.shuffle(train_path)
np.random.shuffle(test_path)

# 用于缓存已加载的图像数据,避免重复加载
DATA_CACHE = {}

# 自定义数据集类 XunFeiDataset
class XunFeiDataset(Dataset):
    def __init__(self, img_path, transform=None):
        self.img_path = img_path
        if transform is not None:
            self.transform = transform
        else:
            self.transform = None
    
    def __getitem__(self, index):
        if self.img_path[index] in DATA_CACHE:
            img = DATA_CACHE[self.img_path[index]]
        else:
            img = nib.load(self.img_path[index]) 
            img = img.dataobj[:,:,:, 0]
            DATA_CACHE[self.img_path[index]] = img
        
        # 随机选择一些通道            
        idx = np.random.choice(range(img.shape[-1]), 50)
        img = img[:, :, idx]
        img = img.astype(np.float32)

        if self.transform is not None:
            img = self.transform(image = img)['image']
        
        img = img.transpose([2,0,1])
        return img, torch.from_numpy(np.array(int('NC' in self.img_path[index])))
    
    def __len__(self):
        return len(self.img_path)

import albumentations as A
# 训练数据加载器,应用数据增强
train_loader = torch.utils.data.DataLoader(
    XunFeiDataset(train_path[:-10],
        A.Compose([
            A.RandomRotate90(),         # 随机旋转90度
            A.RandomCrop(120, 120),     # 随机裁剪
            A.HorizontalFlip(p=0.5),    # 随机水平翻转
            A.RandomContrast(p=0.5),    # 随机对比度调整
            A.RandomBrightnessContrast(p=0.5),  # 随机亮度和对比度调整
        ])
    ), batch_size=2, shuffle=True, num_workers=1, pin_memory=False
)

# 验证数据加载器,应用随机裁剪
val_loader = torch.utils.data.DataLoader(
    XunFeiDataset(train_path[-10:],
        A.Compose([
            A.RandomCrop(120, 120),     # 随机裁剪
        ])
    ), batch_size=2, shuffle=False, num_workers=1, pin_memory=False
)

# 测试数据加载器,应用随机裁剪、水平翻转和对比度调整
test_loader = torch.utils.data.DataLoader(
    XunFeiDataset(test_path,
        A.Compose([
            A.RandomCrop(128, 128),     # 随机裁剪
            A.HorizontalFlip(p=0.5),    # 随机水平翻转
            A.RandomContrast(p=0.5),    # 随机对比度调整
        ])
    ), batch_size=2, shuffle=False, num_workers=1, pin_memory=False
)

在import里面,os模块提供了许多与操作系统交互的功能,用于处理文件和目录、运行外部命令等;sys 模块提供了与 Python 解释器交互的功能,可以访问和操作解释器的相关信息,例如命令行参数、路径、标准输入输出等;argparse 是用于处理命令行参数的模块,允许定义和解析命令行参数,从而使您的脚本可以从命令行接收输入参数,而不必在代码中写死这些参数;tqdm 是一个在循环中显示进度条的模块,有助于在长时间运行的循环中实时跟踪进度,为循环提供了一种视觉反馈,让程序员知道代码在执行过程中的进展情况。
一开始我以为这个代码像baseline那样也可以在线上平台运行,但是运行了以后出现以下提示,说不能引入torch,需要使用paddle paddle创建模型,于是就只能在本地运行,注意要提前把Pytorch,CUDA都配置好,要不然会报很多错。

AI Studio提示
在上面的代码中,类 XunFeiDataset 是自定义的 PyTorch 数据集类,用于加载和处理图像数据。下面是类中的每个方法的作用:
init(self, img_path, transform=None):构造方法,初始化数据集对象。img_path: 图像文件路径列表。transform: 一个数据变换对象,用于对图像应用数据增强。如果传入了 transform,则将其赋值给对象属性self.transform,否则设为 None。
getitem(self, index):用于获取数据集中的一个样本。index: 样本的索引。如果图像路径 self.img_path[index] 已经存在于 DATA_CACHE 中,就直接从缓存中获取图像数据 img;否则,加载 NIfTI 格式的图像数据,提取图像的第一个通道,然后将数据缓存到 DATA_CACHE。随机选择 50 个通道,将其组成一个新的图像 img。如果定义了数据变换(self.transform 不为 None),就将变换应用于图像数据,例如进行数据增强。将图像维度转换为 (通道数, 高度, 宽度) 的形式。返回图像 img 和对应的标签。标签通过检查文件路径中是否包含 ‘NC’ 来判断类别,将其转化为 0 或 1 的整数。
len(self):返回数据集中样本的数量(即图像数量)。这个自定义数据集类的主要作用是将图像加载和处理的过程封装成 PyTorch 数据集对象,以便在训练和评估过程中能够方便地通过数据加载器使用。这种设计使得您可以使用 PyTorch 的数据加载和批处理机制来有效地处理图像数据。

2.自定义模型

class XunFeiNet(nn.Module):
    def __init__(self):
        super(XunFeiNet, self).__init__()
                
        model = models.resnet18(True)
        model.conv1 = torch.nn.Conv2d(50, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
        model.avgpool = nn.AdaptiveAvgPool2d(1)
        model.fc = nn.Linear(512, 2)
        self.resnet = model
        
    def forward(self, img):        
        out = self.resnet(img)
        return out
        
model = XunFeiNet()
model = model.to('cuda')
criterion = nn.CrossEntropyLoss().cuda()
optimizer = torch.optim.AdamW(model.parameters(), 0.001)

使用PyTorch定义一个神经网络模型,用于执行图像分类任务。
XunFeiNet 类定义:继承自 nn.Module。
在构造函数(init)中:通过super(XunFeiNet, self).init() 调用父类构造函数。从 torchvision.models.resnet18(True) 加载一个预训练的ResNet-18模型。True 参数表示模型权重应该使用预训练权重进行初始化。使用 torch.nn.Conv2d 层修改 ResNet 模型的 conv1 层,将输入通道大小修改为50(通常为3),并使用指定的卷积核大小、步幅和填充。将全局平均池化层(avgpool)替换为自适应平均池化层(nn.AdaptiveAvgPool2d(1)),以生成固定大小的输出,与输入大小无关。将末尾的全连接层(fc)替换为输出大小为2的线性层(可能用于二分类)。修改后的 ResNet 模型存储在 self.resnet 属性中。
forward 方法:接受一个图像(img)作为输入。将输入图像通过修改后的 ResNet 模型(self.resnet)。返回模型的输出。
模型实例化和设置:创建 XunFeiNet 模型的一个实例,并将其存储在变量 model 中。然后,使用 .to(‘cuda’) 将模型移动到GPU上(如果有可用的GPU)。
损失和优化器设置:定义交叉熵损失函数(nn.CrossEntropyLoss)。交叉熵损失通常用于分类任务。使用 .cuda() 将损失函数移动到GPU上。使用 AdamW 优化器来优化模型的参数。AdamW 是 Adam 优化器的扩展,包含了权重衰减(L2正则化)。学习率设置为 0.001。

3.模型训练与验证

def train(train_loader, model, criterion, optimizer):
    model.train()
    train_loss = 0.0
    for i, (input, target) in enumerate(train_loader):
        input = input.cuda(non_blocking=True)
        target = target.cuda(non_blocking=True)

        output = model(input)
        loss = criterion(output, target)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if i % 20 == 0:
            print(loss.item())
            
        train_loss += loss.item()
    
    return train_loss/len(train_loader)
            
def validate(val_loader, model, criterion):
    model.eval()
    val_acc = 0.0
    
    with torch.no_grad():
        for i, (input, target) in enumerate(val_loader):
            input = input.cuda()
            target = target.cuda()

            # compute output
            output = model(input)
            loss = criterion(output, target)
            
            val_acc += (output.argmax(1) == target).sum().item()
            
    return val_acc / len(val_loader.dataset)
    
for _  in range(3):
    train_loss = train(train_loader, model, criterion, optimizer)
    val_acc  = validate(val_loader, model, criterion)
    train_acc = validate(train_loader, model, criterion)
    
    print(train_loss, train_acc, val_acc)

第三部分主要分为2个函数(train()和validate())和一个循环:
train 函数:
接受训练数据加载器 (train_loader)、模型 (model)、损失函数 (criterion) 和优化器 (optimizer) 作为输入。将模型设置为训练模式,即调用 model.train()。初始化训练损失为0。遍历训练数据加载器,处理每个批次:将输入和目标移动到GPU上。通过模型计算输出,计算损失。清零优化器的梯度,反向传播计算梯度,执行优化器的更新步骤。如果当前批次的索引是20的倍数,打印当前的损失。累积训练损失。最后,返回平均训练损失。
validate 函数:
接受验证数据加载器 (val_loader)、模型 (model) 和损失函数 (criterion) 作为输入。将模型设置为评估模式,即调用 model.eval()。初始化验证准确率为0。使用 torch.no_grad() 上下文,遍历验证数据加载器,处理每个批次:将输入和目标移动到GPU上。通过模型计算输出。计算损失。根据输出和目标计算批次中正确预测的数量,并累积到验证准确率。最后,返回验证准确率。
循环部分:
在一个循环中,执行3个周期(epoch)的训练和验证。在每个周期中,首先调用 train 函数进行训练,并获取训练损失。然后,调用 validate 函数计算验证准确率,同时还计算了在训练数据上的准确率。最后,打印出训练损失、训练准确率和验证准确率。

4.模型预测与提交

def predict(test_loader, model, criterion):
    model.eval()
    val_acc = 0.0
    
    test_pred = []
    with torch.no_grad():
        for i, (input, target) in enumerate(test_loader):
            input = input.cuda()
            target = target.cuda()

            output = model(input)
            test_pred.append(output.data.cpu().numpy())
            
    return np.vstack(test_pred)
    
pred = None
for _ in range(10):
    if pred is None:
        pred = predict(test_loader, model, criterion)
    else:
        pred += predict(test_loader, model, criterion)
        
submit = pd.DataFrame(
    {
        'uuid': [int(x.split('/')[-1][:-4]) for x in test_path],
        'label': pred.argmax(1)
})
submit['label'] = submit['label'].map({1:'NC', 0: 'MCI'})
submit = submit.sort_values(by='uuid')
submit.to_csv('submit2.csv', index=None)

predict 函数:
接受测试数据加载器 (test_loader)、模型 (model) 和损失函数 (criterion) 作为输入。将模型设置为评估模式,即调用 model.eval()。初始化测试预测列表 test_pred。使用 torch.no_grad() 上下文,遍历测试数据加载器,处理每个批次:将输入和目标移动到GPU上。通过模型计算输出。将输出的数据转换为NumPy数组并添加到 test_pred 列表中。最后,将所有预测结果堆叠为一个NumPy数组并返回。
循环部分:
在一个循环中,重复10次(或者你想要的次数)。在每次循环中,使用 predict 函数在测试数据上进行预测,获得预测结果。如果是第一次循环,则直接将预测结果赋值给 pred。如果不是第一次循环,则将当前预测结果累加到之前的预测结果中(pred += predict(test_loader, model, criterion))。
提交文件生成部分:
在所有循环结束后,pred 包含了累积的预测结果。创建一个 pandas DataFrame submit,包含 ‘uuid’ 列(从测试路径中提取的文件名)和 ‘label’ 列(预测的标签)。将预测的标签从数字映射为相应的字符串标签(‘MCI’ 和 ‘NC’)。对DataFrame按照 ‘uuid’ 列进行排序。将DataFrame保存为名为 ‘submit2.csv’ 的CSV文件,其中包含 ‘uuid’ 和 ‘label’ 列,用于提交。

二、使用PP套件

可以参考讲解文档:使用paddle paddle套件代码
感觉里面讲得非常的详细,里面将nii图片转为jpg图片,然后对数据集进行划分生成3个文件,分别为labels.txt, train_list.txt, val_list.txt,最后直接进行模型训练,就是训练出来的结果也不是很高,可能还需要任务三那些优化方法。
评测结果

三、反思

Pytorch CNN 实例代码里用的是基于ResNet-18架构的卷积神经网络(CNN)模型。这种模型在图像分类任务中被广泛应用。但经过搜索查找,发现这个模型的一些优缺点:
优点:
1.特征提取能力强大:ResNet-18作为一个深度的卷积神经网络,具有强大的特征提取能力。它可以自动从图像中学习抽取有用的特征,这对于图像分类等任务非常有益。
2.预训练权重:代码中使用了预训练的ResNet-18模型,这意味着模型的初始权重是在大规模图像数据上进行训练得到的。这些预训练的权重可以在一定程度上提高模型的性能,特别是在数据有限的情况下。
3.自适应平均池化:将全局平均池化替换为自适应平均池化可以使模型适应不同大小的输入图像,从而增强了模型的泛化能力。
4.训练和验证循环:代码中的训练和验证循环实现了完整的训练流程,包括损失计算、反向传播和优化。这使得你可以轻松地对模型进行训练和评估。
缺点:
1.计算资源需求:深层的ResNet-18模型可能需要较大的计算资源,尤其是在GPU上进行训练。这可能对资源有限的环境造成一定的挑战。
2.过拟合风险:虽然预训练权重可以缓解过拟合问题,但仍然需要小心监控模型的性能,以避免在训练集上获得较好的表现而在未见过的数据上表现不佳。
3.超参数调整:代码中的学习率、优化器等超参数可能需要进行调整,以便获得最佳的模型性能。超参数调整可能需要一些实验和经验。
4.数据预处理:代码中没有显示数据预处理的部分,但图像数据通常需要进行归一化、增强等预处理操作,以提升模型的性能。

总结

以上就是今天所要记录的内容,本文介绍了任务二中代码的含义和内在逻辑,并且分析了所用模型的优缺点,现在只是能够大致看懂,CNN模型的应用还要通过日积月累的练习来逐渐掌握。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值