CIFAR10图像分类ResNet模型实战(pytorch)

      Kaggle中【CIFAR-10 - Object Recognition in Images】竞赛实战,小白自学参考ResNet-18实现Cifar-10图像分类(测试集分类准确率95.170%)代码,熟悉了解每一步功能作用及思路。
参考2:pytorch入坑笔记1: 从ResNet出发引发的几点思考

1 ResNet18网络实现及略微改进

      首先根据下图利用pyTorch实现ResNet网络,这里需要注意原论文中采用的数据集为ImageNet数据集,输入图像数据大小224×224,第一个卷积核大小kernel_size=7,而CIFAR10数据集的输入图像大小为32×32,故这里将卷积核大小改为kernel_size=3。这里也没有加入最大池化层maxpool,可能是因为32×32的输入比较小,一开始使用maxpool可能会损失较多信息,该结构在224×224下表现应该比较正常,故这里做了些调整。
在这里插入图片描述

# 1 定义ResNet18网络
# 1.1 定义残差块
class ResidualBlock(nn.Module):
    def __init__(self, inchannel, outchannel, stride=1):    # 残差块可以指定输入通道数、输出通道数、步长
        super(ResidualBlock, self).__init__()
        self.left = nn.Sequential(
            nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(outchannel),
            nn.ReLU(inplace=True),
            nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(outchannel)
        )
        self.shortcut = nn.Sequential()
        # 条件stride != 1指在每个残差块连接时,如果发生了尺寸减半、通道数增倍的情况下,参数s均取2, 其余情况均为s=1
        # 条件inchannel != outchannel指上一个残差块的输入与这个残差块本身的的输出不同时,即需要1×1卷积来完成通道数增倍
        # 将inchannel直接连接1×1卷积层将输入改变大小(s2=)及通道数(#filter个数)
        if stride != 1 or inchannel != outchannel:
            self.shortcut = nn.Sequential(
                nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(outchannel)
            )

    def forward(self, x):
        out = self.left(x)
        out += self.shortcut(x)
        out = F.relu(out)
        return out

# 1.2 定义ResNet整个网络模型
class ResNet(nn.Module):
    # 输入参数为残差块、
    def __init__(self, ResidualBlock, num_classes=10):
        super(ResNet, self).__init__()
        self.inchannel = 64
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(),
        )
        self.layer1 = self.make_layer(ResidualBlock, 64,  2, stride=1)
        self.layer2 = self.make_layer(ResidualBlock, 128, 2, stride=2)
        self.layer3 = self.make_layer(ResidualBlock, 256, 2, stride=2)
        self.layer4 = self.make_layer(ResidualBlock, 512, 2, stride=2)
        self.fc = nn.Linear(512, num_classes)

    def make_layer(self, block, channels, num_blocks, stride):
        strides = [stride] + [1] * (num_blocks - 1)   #strides=[1,1]
        # 第一个ResidualBlock的步幅由make_layer的函数参数stride指定
        # ,后续的num_blocks-1个ResidualBlock步幅是1
        layers = []
        for stride in strides:
            layers.append(block(self.inchannel, channels, stride))
            self.inchannel = channels  # 第一个残差块之后的每一个残差块的输入通道数都等于上一个残差块额输出通道数
        return nn.Sequential(*layers)

    def forward(self, x):
        out = self.conv1(x)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = F.avg_pool2d(out, 4)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out

def ResNet18():
    return ResNet(ResidualBlock)

2 学习过程中的部分问题总结:

2.1 为什么nn.ReLU() 设置 inplace=True?

      ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。可以理解为就地操作。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度,但是只有少数的autograd操作支持inplace操作(如tensor.sigmoid_()),一般建议不要使用inplace操作。

在 pytorch 中, 有两种情况不能使用 inplace operation:

  1. 对于 requires_grad=True 的叶子张量(leaf tensor) 不能使用inplace operation
  2. 对于在求梯度阶段需要用到的张量 ,不能使用 inplace operation

2.2 nn.Sequential(*layers)加了一个*

layers = []
layers.append(nn.Linear())
··· # 可以继续append网络层
return nn.Sequential(*layers)

如果*号加在了是实参上,代表的是将输入迭代器拆成一个个元素

2.3 net.train()/ net.eval()

      使用PyTorch进行训练和测试时,要把实例化的模型设定为train() / eval()模式。当net.eval()时,框架会自动把BNDropOut固定住,不会取平均,而是使用训练好的值。
      对于BN层来说, 在训练过程中,对每一个batch取一个样本均值和方差,然后使用滑动指数平均所有的batch的均值和方差来近似整个样本的均值和方差。 对于测试阶段,固定样本和方差后,BN相当于一个线性的映射关系。所以说对于pytorch来说,在训练阶段设置 net.train() 相当于打开滑动指数平均按钮,不断的更新;测试阶段设置 net.eval()关闭更新,相当于一个线性映射关系。
      Dropout是概率权重衰减,在测试时为了得到准确结果而非一个概率解,故采用 net.train() / net.eval()来分别对应训练和测试阶段。

2.4 用到的argsparse模块

# 参数设置,使得我们能够手动输入命令行参数,就是让风格变得和Linux命令行差不多
parser = argparse.ArgumentParser(description='PyTorch CIFAR10 Training')
parser.add_argument('--outf', default='./model/', help='folder to output images and model checkpoints') #输出结果保存路径
args = parser.parse_args()

      代码中的这一部分的目的是使得该项目可以通过命令行来执行py文件并传入相关参数,即可以不必打开pycharm等其他解释器,在命令行终端传入所需参数并运行即可。
当在命令行中输入:python cifar10.py --help 时,命令行中出现:

usage: cifar10.py [-h] --outf

PyTorch CIFAR10 Training

positional arguments:
--outf   older to output images and model checkpoints

      在命令行中输入python cifar10.py "./model/"即表示该文件夹路径"./model/"为输入参数,该文件夹指的是输出结果的保存路径;default='./model/'表示当没有输入参数时,参数--outf的默认值为'./model/'
args = parser.parse_args()表示将改参数保存到args当中,后续会用到该参数。

      在开始训练时的两行代码:

if not os.path.exists(args.outf): 
    os.makedirs(args.outf)

      即用到了参数args,表示如果当前工作目录不存在args.outf表示的这个文件夹,则在当前工作目录下创建一个文件夹,该文件夹的名字默认为model

2.5 创建记录数据的txt文件

      在if __name__ == "__main__":语句下的with open("acc.txt", "w") as f:表示以写方式打开当前工作目录名为acc.txt的文件,若不存在则系统创建一个;
     语句with open("log.txt", "w")as f2:同理。

2.6 sum_loss 、 predicted 、total 、correct (重点理解)

sum_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += predicted.eq(labels.data).cpu().sum()

      对于训练过程中的每一个batch,loss.item()表示每一个batch计算出的loss,每一次迭代sum_loss不断加上loss.item(), 最终输出为sum_loss / (i + 1)
output.data为输入经过网络模型后的输出数据,对于此项目,batch_size=128,网络输出通道数为10,所以output.data.shape = torch.Size([128, 10]), 其中随意截取一个output.data表示为:

output.data tensor([[-0.3080,  0.7289, -0.4223,  ..., -0.0324,  1.1634, -1.3871],
        [-0.4914,  0.6484, -0.3070,  ..., -0.2455,  1.3141, -1.3008],
        [-0.0460, -0.0178, -0.3106,  ..., -0.1473,  0.6722, -0.5494],
        ...,
        [ 0.1572,  0.2035, -0.3714,  ..., -0.1039,  0.6811, -0.5764],
        [ 0.0404,  0.8012, -0.4957,  ...,  0.0377,  1.2612, -1.0142],
        [-0.0138,  0.0319, -0.2495,  ..., -0.1314,  0.6073, -0.6382]],
       device='cuda:0') torch.Size([128, 10])

      每一行表示对于一张图片,output.data对应一个batch的全部输出。网络模型给出的该图片对应10种分类中的10个概率,最终认为概率最大的那个数所对应的类型即为模型判断出的该图片所属类型。
_, predicted = torch.max(outputs.data, 1)
      torch.max(input, dim)函数输入一个tensor, dim=1表示取每行的最大值,该函数会返回两个tensor:第一个tensor是每行的最大值;第二个tensor是每行最大值的索引。我们这里仅需要每行最大值的索引数而不需要每行的概率最大值,故返回值取_, predicted。其中随意截取一个predicted表示为:

tensor([8, 8, 8, 8, 8, 8, 8, 8, 5, 8, 8, 8, 8, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
        8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 1, 8, 8, 8, 8, 8, 5, 8, 8, 8, 8,
        8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 1, 8, 8, 1,
        8, 8, 8, 5, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 5, 8, 8, 8, 8, 8, 8, 8,
        8, 8, 8, 8, 8, 8, 8, 8, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 5, 8, 8, 8,
        8, 8, 8, 8, 8, 8, 8, 5], device='cuda:0')
torch.Size([128])

labels.size() = torch.Size([128]),所以total += labels.size(0)表示保存所遍历过的图片总数目。
      predicted.eq(labels.data)表示将预测的结果与labels.data的真实值进行比较(predicted、labels.data均为一个tensor,且tensor.size() = ([128]) ),ep()函数返回一个同predicted相同大小的tensor,其中的每一个值相同返回True,不同返回False。其中随意截取一个labels.data表示为:

label.data tensor([7, 9, 3, 1, 4, 8, 0, 8, 2, 5, 6, 1, 7, 5, 2, 9, 5, 3, 8, 4, 8, 1, 6, 9,
        7, 7, 8, 6, 1, 6, 9, 7, 5, 8, 9, 7, 0, 8, 3, 5, 0, 2, 6, 3, 6, 8, 4, 4,
        4, 6, 6, 0, 4, 8, 8, 4, 3, 7, 2, 1, 7, 5, 4, 1, 2, 7, 5, 0, 4, 7, 9, 3,
        0, 0, 3, 7, 8, 4, 4, 2, 0, 5, 0, 8, 4, 2, 7, 4, 3, 0, 3, 0, 7, 7, 9, 8,
        0, 5, 4, 4, 6, 4, 6, 8, 1, 4, 2, 7, 7, 3, 0, 5, 8, 4, 6, 3, 3, 9, 4, 0,
        8, 4, 5, 8, 0, 7, 1, 7], device='cuda:0')

      所对应的predicted.eq(labels.data)表示为:

tensor([False, False, False, False, False,  True, False,  True, False, False,
        False, False, False,  True, False, False, False, False,  True, False,
         True, False, False, False, False, False,  True, False, False, False,
        False, False, False,  True, False, False, False, False, False, False,
        False, False, False, False, False,  True, False, False, False, False,
        False, False, False,  True,  True, False, False, False, False, False,
        False, False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False, False, False, False,
        False, False, False,  True, False, False, False, False, False, False,
        False, False, False, False, False,  True, False, False, False, False,
        False, False, False,  True, False, False, False, False, False, False,
        False, False,  True, False, False, False, False, False, False, False,
         True, False, False,  True, False, False, False, False],
       device='cuda:0')

      correct += predicted.eq(labels.data).cpu().sum()sum()函数返回ep()函数返回tensor中结果为True的个数,可以看到predicted.eq(labels.data)返回的True的个数有16个,故该函数返回correct tensor(16.)

2.7 __init__中使用nn.Relu(),forward中使用F.relu(),两者的区别?

英文文档的解释:

How to choose between torch.nn.Functional and torch.nn module?

In PyTorch you define your Models as subclasses of torch.nn.Module.
In the init function, you are supposed to initialize the layers you want to use. Unlike keras, Pytorch goes more low level and you have to specify the sizes of your network so that everything matches.
In the forward method, you specify the connections of your layers. This means that you will use the layers you already initialized, in order to re-use the same layer for each forward pass of data you make.
torch.nn.Functional contains some useful functions like activation functions a convolution operations you can use. However, these are not full layers so if you want to specify a layer of any kind you should use torch.nn.Module.
You would use the torch.nn.Functional conv operations to define a custom layer for example with a convolution operation, but not to define a standard convolution layer.

     也就是说__init__定义的是标准层,比如这里nn.Relu()是标准层,而在forward里面是用的F.relu()更像是一种操作,不改变网络的参数权值。

3 源代码附详细解释

# 2 参数设置,使得我们能够手动输入命令行参数,就是让风格变得和Linux命令行差不多
parser = argparse.ArgumentParser(description='PyTorch CIFAR10 Training')
parser.add_argument('--outf', default='./model/', help='folder to output images and model checkpoints') #输出结果保存路径
args = parser.parse_args()

# 2.1 超参数设置
EPOCH = 135   #遍历数据集次数
pre_epoch = 0  # 定义已经遍历数据集的次数
BATCH_SIZE = 128      #批处理尺寸(batch_size)
LR = 0.1        #学习率

# 3 数据获取及预处理
transform = transforms.Compose([transforms.Resize((32,32)),
                               transforms.ToTensor(),
                               transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) #R,G,B每层的归一化用到的均值和方差
                               ])

training_data = torchvision.datasets.CIFAR10(root='./data', train=True, download=False, transform=transform)  # 50000个训练集
test_date = torchvision.datasets.CIFAR10(root='./data', train=False, download=False, transform=transform)     # 10000个测试集

train_iter = torch.utils.data.DataLoader(training_data, batch_size=BATCH_SIZE, shuffle=True)
test_iter = torch.utils.data.DataLoader(test_date, batch_size=BATCH_SIZE, shuffle=True)

# Cifar-10的标签
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# 4 模型定义-ResNet
net = ResNet18().to(device)

# 5 定义损失函数和优化方式
criterion = nn.CrossEntropyLoss()  # 损失函数为交叉熵,多用于多分类问题
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9, weight_decay=5e-4) #优化方式为mini-batch momentum-SGD,并采用L2正则化(权重衰减)

# 6 训练
if __name__ == "__main__":
    if not os.path.exists(args.outf):
        os.makedirs(args.outf)
    best_acc = 85  #2 假设一个best test accuracy
    print("Start Training, Resnet-18!")
    with open("acc.txt", "w") as f:
        with open("log.txt", "w")as f2:
            for epoch in range(pre_epoch, EPOCH):
                print('\nEpoch: %d' % (epoch + 1))     # 每开始一次新的epoch时,显示一次;
                net.train()
                sum_loss = 0.0
                correct = 0.0
                total = 0.0
                for i, data in enumerate(train_iter, 0):
                    # 用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,
                    # 参数0表示下标起始位置为0,返回 enumerate(枚举) 对象。
                    # 准备数据
                    length = len(train_iter)
                    inputs, labels = data
                    inputs, labels = inputs.to(device), labels.to(device)
                    optimizer.zero_grad()

                    # forward + backward
                    outputs = net(inputs)
                    loss = criterion(outputs, labels)
                    loss.backward()
                    optimizer.step()

                    # 每训练1个batch打印一次loss和准确率
                    sum_loss += loss.item()
                    # 取得分最高的那个类 (outputs.data的索引号)
                    _, predicted = torch.max(outputs.data, 1)
                    total += labels.size(0)
                    print('output.data', outputs.data, outputs.data.shape)
                    print("loss.item()", loss.item())
                    print('predicted = torch.max(outputs.data, 1):', predicted)
                    print("labels.size(0):", labels.size())
                    print("total:", total)
                    correct += predicted.eq(labels.data).cpu().sum()
                    print('label.data', labels.data)
                    print('correct += predicted.eq(labels.data).cpu().sum()', predicted.eq(labels.data))
                    print("correct", correct)

                    print('[epoch:%d, iter:%d] Loss: %.03f | Acc: %.3f%% '
                          % (epoch + 1, (i + 1 + epoch * length), sum_loss / (i + 1), 100. * correct / total))
                    f2.write('%03d  %05d |Loss: %.03f | Acc: %.3f%% '
                          % (epoch + 1, (i + 1 + epoch * length), sum_loss / (i + 1), 100. * correct / total))
                    f2.write('\n')
                    f2.flush()

                # 每训练完一个epoch测试一下准确率
                print("Waiting Test!")
                with torch.no_grad():
                    correct = 0
                    total = 0
                    for data in test_iter:
                        net.eval()
                        images, labels = data
                        images, labels = images.to(device), labels.to(device)
                        outputs = net(images)
                        # 取得分最高的那个类 (outputs.data的索引号)
                        _, predicted = torch.max(outputs.data, 1)
                        total += labels.size(0)
                        correct += (predicted == labels).sum()
                    print('测试分类准确率为:%.3f%%' % (100 * correct / total))
                    acc = 100. * correct / total
                    # 将每次测试结果实时写入acc.txt文件中
                    print('Saving model......')
                    torch.save(net.state_dict(), '%s/net_%03d.pth' % (args.outf, epoch + 1))
                    f.write("EPOCH=%03d,Accuracy= %.3f%%" % (epoch + 1, acc))
                    f.write('\n')
                    f.flush()
                    # 记录最佳测试分类准确率并写入best_acc.txt文件中
                    if acc > best_acc:
                        f3 = open("best_acc.txt", "w")
                        f3.write("EPOCH=%d,best_acc= %.3f%%" % (epoch + 1, acc))
                        f3.close()
                        best_acc = acc
            print("Training Finished, TotalEPOCH=%d" % EPOCH)

欢迎关注【OAOA

  • 11
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: CIFAR-10是一个常用的图像分类数据集,其中包含10个类别的60000张彩色图像,每个类别6000张,图像大小为32x32像素。而ResNet-18是一种深度卷积神经网络架构,被广泛应用于图像分类任务。 ResNet-18由多个卷积层、池化层和全连接层组成,其中包含18个卷积层。与普通的卷积神经网络不同,ResNet-18引入了残差连接,通过直接将输入信息添加到网络输出中,可以更好地解决梯度消失和网络退化问题。这些残差连接可以保持梯度的流动,允许网络更深层次地进行训练。 在CIFAR-10图像分类任务中使用ResNet-18时,我们可以将图像作为输入,经过一系列的卷积操作和池化操作后,通过全连接层输出对应的类别概率。通过训练,网络会学习到适当的卷积核权重,以在图像中提取关键特征。这些特征被用来对图像进行分类,并预测其所属的类别。 ResNet-18的优点是具有较低的参数量和计算复杂度,同时具备较强的表达能力。CIFAR-10数据集相对较小,使用ResNet-18可以有效地提取图像的特征,并取得较好的分类效果。但在实际应用中,如果需要处理更大规模的图像分类任务,可能需要使用更深层次的ResNet网络或其他更为复杂的模型。 总之,通过使用ResNet-18对CIFAR-10数据集进行图像分类,可以获得较好的分类性能,通过深度的卷积神经网络结构和残差连接的设计,可以提取出图像中的有价值的特征信息,并实现对不同类别图像的有效分类。 ### 回答2: CIFAR-10是一个包含10个不同类别的图像数据集,常用于图像分类任务的基准测试。ResNet-18是一种深度卷积神经网络架构,适用于处理图像分类问题。 ResNet-18主要由18层堆叠的卷积神经网络组成,具有残差连接。它通过在网络中引入跨层的汇集路径,解决了梯度消失和网络难以训练的问题。这使得网络能够更深更容易训练,提高了分类准确性。 对于CIFAR-10图像分类任务,我们可以使用ResNet-18模型来进行训练和预测。首先,我们需要将CIFAR-10数据集进行预处理,包括图像归一化和标签处理。然后,我们可以使用ResNet-18模型的预训练权重或从头开始训练网络。 在训练过程中,我们将输入图像传递给ResNet-18网络,通过一系列卷积、汇集和全连接层,进行特征提取和图像分类。通过使用反向传播算法,我们可以根据真实标签和网络输出之间的差异,来更新网络的权重和偏置,不断优化网络。 在预测过程中,我们将测试图像输入ResNet-18网络,得到网络的输出概率分布。根据最高概率的类别标签,我们可以将图像分类为对应的类别。通过评估预测结果和真实标签之间的准确性,我们可以衡量ResNet-18模型在CIFAR-10图像分类任务上的性能。 总之,CIFAR-10图像分类任务中的ResNet-18模型是一种有效的深度学习模型,可以用于提高图像分类的准确性。通过适当的数据预处理、训练和预测过程,我们可以使用ResNet-18模型对CIFAR-10数据集进行图像分类。 ### 回答3: CIFAR-10图像分类是一个常用的计算机视觉任务,而ResNet-18是其中一种常用的深度学习模型ResNet-18是由微软研究院提出的一种卷积神经网络架构,主要解决了深度神经网络训练过程中出现的梯度消失、特征难以传递等问题。 ResNet-18的整体结构包括18层卷积层,其中包括16个普通的卷积层和2个全连接层。该模型使用了残差块的结构,即引入了跳跃连接,使得网络能够通过规模较小的子网络来学习残差,从而更好地学习到图像的特征。 针对CIFAR-10数据集,ResNet-18的输入是32×32大小的RGB彩色图像。模型首先使用一个卷积层对输入图像进行下采样,然后通过若干个残差模块进行特征提取。每个残差模块包含两个卷积层,其中一个卷积层的核大小为3×3,另一个卷积层为1×1,除了第一个残差模块外,每个残差模块都会对输入进行下采样。残差模块之间使用ReLU激活函数进行激活。在卷积层之前和全连接层之后,ResNet-18使用了批归一化和全局平均池化层进行特征处理。 在图像分类任务中,CIFAR-10数据集包含10个类别,包括飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。通过将CIFAR-10数据集作为训练集,用ResNet-18模型进行训练,可以建立一个图像分类器,将输入的图像正确分类为10个类别之一。 总的来说,ResNet-18是一种有效的深度学习模型,对于CIFAR-10图像分类任务具有较好的性能。该模型能够通过引入残差块来解决深度神经网络的梯度消失和特征传递问题,从而提高了模型的准确率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值