基于Resent的医学数据集分类实战

基于Resent的医学数据集分类实战

一.医学疾病数据集介绍

​ 原始数据集:

  1. NCT-CRC-HE-100K:结肠癌组织
    QQ_1721287053122

  2. NIH-ChestXray14 dataset:肺部X光数据

    QQ_1721287117490

  3. HAM10000:皮肤病变数据集

    QQ_1721287171731

​ 但是这些数据集的规模都太大了,所以我们对这些数据进行处理,得到简易实验的数据集,所有的数据格式都类似mnist(28*28),方便我们进行初步试验。
overview

二.Resnet网络架构原理分析

​ Resnet要解决的问题:传统网络模型如果堆叠的越深,效果反而开始下降(由下图可以看出,56层的网络效果反而差于20层的网络),这可怎么办?
QQ_1721287545675

​ Resnet核心模块:

  • 一句话描述:至少不比原来差
  • 相当于加了一个双保险,让模型自己选
  • 继续堆叠很多的这样的结构,网络就能更深
  • 应用领域广泛,各大计算机视觉任务都是以它为基础模块

image-20240718153050232

​ Resnet的应用效果:

  • 可以继续堆叠网络层数,效果还会继续提升
    QQ_1721287953707

三.dataloader加载数据集

​ 我们先看下我们的数据,这里已经将他们全部都打包成.npz文件了,我们在代码中直接加载即可:
QQ_1721288195574

​ 接下来我们来看训练的代码了,先看main函数:

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='RUN Baseline model of MedMNIST')
    parser.add_argument('--data_name', default='pathmnist', help='subset of MedMNIST', type=str)
    parser.add_argument('--input_root', default='./input', help='input root, the source of dataset files', type=str)
    parser.add_argument('--output_root', default='./output', help='output root, where to save models and results',type=str)
    parser.add_argument('--num_epoch', default=100, help='num of epochs of training', type=int)
    parser.add_argument('--download', default=False, help='whether download the dataset or not',type=bool)
    args = parser.parse_args()
    data_name = args.data_name.lower()
    input_root = args.input_root
    output_root = args.output_root
    end_epoch = args.num_epoch
    download = args.download
    main(data_name, input_root, output_root, end_epoch=end_epoch, download=download)

​ 这里主要是命令行参数和相应的解析,- -data_name是我们需要使用的数据集的名字,- -input_root是我们需要使用的数据集的路径,- -output_root是输出结果的路径,- -num_epoch是训练所需的轮次,- -download是是否需要下载数据集,这里指定false即可,因为数据集我们已经提前下载到本地了。
​ 这里我配置的命令行参数为:

- -data_name pathmnist
- -input_root ./medmnist
- -num_epoch 100

​ 这里我们给代码打上一些断点,开始运行程序,一步一步的去看代码中怎么实现的,我们现在项目中的def main函数的第一行打上断点,然后开始调试:
QQ_1721289579906

with open(INFO, 'r') as f:
	info = json.load(f)
	task = info[flag]['task']
	n_channels = info[flag]['n_channels']
	n_classes = len(info[flag]['label'])

​ 这里会打开一个json文件,json文件中的内容为数据集的具体详细的一些配置信息。
QQ_1721291206758

​ 然后我们主要项目中的这三个代码包:

QQ_1721290890290

​ 我们在dataset.py中打上断点,在__getitem__函数的第一行打上断点,然后调试程序:
QQ_1721290994253

​ 我们的getitem函数会依据batch来构建数据,接下来我们来看看它是如何实现的:

def __getitem__(self, index):
	img, target = self.img[index], self.label[index].astype(int)
	img = Image.fromarray(np.uint8(img))
	if self.transform is not None:
		img = self.transform(img)
	if self.target_transform is not None:
		target = self.target_transform(target)
	return img, target

​ 这里的img是数据x,target是它的标签y,我们需要对x进行数据增强的处理(通过transform),对于标签y一般不需要。这里的getitem会对每条数据都进行处理,所以会被调用很多很多次。直到加载完所有的数据。

四.Resnet网络前向传播

​ 此处我们就不用再看dataset.py里面的代码了,我们去models.py中找到resnet的前向传播(forword函数)的代码,并且打上断点。
QQ_1721291938918

​ 我们进行调试,首先看一下输入数据的规模:
QQ_1721292396488

五.残差网络的shortcut操作

​ 我们走了两层之后,发现特征图大小变成了:128*64*28*28,其实走一层的时候就变成这样了。batch和h和w都是没有变的。我们设置stride为2的时候,h和w一般才会变。为什么经过一层的卷积后,会得到64张特征图呢?QQ_1721292749508

​ 答案在这里,因为有64个卷积核,所以经过一层卷积后会得到64张特征图:
QQ_1721292951603

​ 接下来我们来刨析一下Layer1层的行为,根据调试结果不难发现,Layer1并没有让[b,c,h,w]发生变换,我们来分析下原因:
QQ_1721349534153

​ 我们首先发现,layer1到layer4都是我们自己定义的一个make_layer函数,那么这个make_layer又是干什么的呢?我们先看代码:

def _make_layer(self, block, planes, num_blocks, stride):
	strides = [stride] + [1]*(num_blocks-1)
	layers = []
	for stride in strides:
        layers.append(block(self.in_planes, planes, stride))
        self.in_planes = planes * block.expansion
    return nn.Sequential(*layers)

​ 这里传入的第一个参数block表示什么意思,这个参数是表示一个基础的计算单元是什么,我们的resnet网络是分为一个个BasicBlock去计算的:

class BasicBlock(nn.Module):
	def forward(self, x):
    #print(x.shape)
    out = F.relu(self.bn1(self.conv1(x)))
    #print(out.shape)
    out = self.bn2(self.conv2(out))
    #print(out.shape)
    out += self.shortcut(x)
    #print(out.shape)
    out = F.relu(out)
    return out

​ 这里有个BasciBlock的计算过程是两次卷积操作(bn1和bn2),和一次shortcut操作。计算流程如下:
QQ_1721350065654

​ 传入的第二个参数是输出图像的channel通道数目,第三个参数num_block[],表示做几组这样的Basci_Block。最后的stride是卷积时的步长,会影响到h和w。
​ 因为我们控制了Basic_Block中的两次卷积后的结果保持和输入一样的规模,所以需要将输入加入到输出的卷积结果上,但是这里在加上输入的时候为什么还要做一个shortcut操作呢?我们来看看shortcut操作是在干什么:

 self.shortcut = nn.Sequential()
 if stride != 1 or in_planes != self.expansion*planes:
 	self.shortcut = nn.Sequential(
        nn.Conv2d(in_planes, self.expansion*planes,
                          kernel_size=1, stride=stride, bias=False),				nn.BatchNorm2d(self.expansion*planes)
    )

​ 这里的shortcut居然是一个空序列!只有当stride和输入图像的通道数满足一定的条件时,Sequential中才会进行一些操作。所以这里代码在layer1中执行num_block[0]个Basic_Block后【根据上面的图可以看出来是两个】,也就运行结束了。

六.特征图升维与降采样操作

​ layer1我们就此分析结束,下面我们来分析layer2,layer2就很不同了,因为它的in_channel和out_channel是不一样的!
QQ_1721360778578

​ 我们发现现在输入和输出的尺寸不一样了,这时候我们应该怎么将输入加到输出上呢?还记得我们之前实现的shortcut函数吗,这个时候就发挥用处啦!
​ 如果stride不为1或者输入的channel和输出的channel不匹配的时候,这里会将之前定义的空的序列替换为一个in_channel为64,out_channel为128,stride=(2,2)的卷积层【升维,下采样】。使得输入与输出可以进行加法运算。这里也对应着下图的操作。
QQ_1721361308296

七.网络整体流程与训练演示

​ 接下来我们继续分析layer3,做法还是跟之前是一样的,这里继续进一步扩大维度然后下采样,通过shortcut连接1*1的卷积,将每一层的输入与输出相加。
QQ_1721371870921

​ 最后四层网络都走完了,h和w都下采样变为了1,通过一个View操作拉直:
QQ_1721371938471

QQ_1721372013980

​ 最后通过一个线性层,得到我们的9分类结果:

QQ_1721372112382

​ 到此我们的网络结果就讲解完了,接下来我们来看训练的步骤:

model.train()
for batch_idx, (inputs, targets) in enumerate(train_loader):
	optimizer.zero_grad()
	outputs = model(inputs.to(device))
    if task == 'multi-label, binary-class':
       	targets = targets.to(torch.float32).to(device)
       	loss = criterion(outputs, targets)
    else:
       	targets = targets.squeeze().long().to(device)
       	loss = criterion(outputs, targets)
    loss.backward()
    optimizer.step()

​ 我们刚刚将的网络流程全部都在outputs = model(inputs.to(device))这行代码中走完了,接下来就是常规的计算loss,然后反向传播,更新梯度。然后再进行下一批次的训练。主函数中通过按照epoch调用:

for epoch in trange(start_epoch, end_epoch):
	train(model, optimizer, criterion, train_loader, device, task)
	val(model, val_loader, device, val_auc_list, task, dir_path, epoch)

​ val部分跟trian部分代码差别不大,只是不用更新梯度了,需要计算一些指标:

 model.eval() #进入val模式
    y_true = torch.tensor([]).to(device)
    y_score = torch.tensor([]).to(device)
    with torch.no_grad():
        for batch_idx, (inputs, targets) in enumerate(val_loader):
            outputs = model(inputs.to(device))
            if task == 'multi-label, binary-class':
                targets = targets.to(torch.float32).to(device)
                m = nn.Sigmoid()
                outputs = m(outputs).to(device)
            else:
                targets = targets.squeeze().long().to(device)
                m = nn.Softmax(dim=1)
                outputs = m(outputs).to(device)
                targets = targets.float().resize_(len(targets), 1)
            y_true = torch.cat((y_true, targets), 0)
            y_score = torch.cat((y_score, outputs), 0)
        y_true = y_true.cpu().numpy()
        y_score = y_score.detach().cpu().numpy()
        auc = getAUC(y_true, y_score, task) #计算指标
        val_auc_list.append(auc)

​ 跑完之后,我们就可以拿到训练的结果【每个epoch的结果】,然后打开项目中的evaluator.py,可以对我们的训练结果进行评估。
QQ_1721372765066

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Li小李同学Li

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值