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

  • 1 ResNet18网络实现及略微改进
  • 2 学习过程中的部分问题总结:
  • 2.1 为什么nn.ReLU() 设置 inplace=True?
  • 2.2 nn.Sequential(*layers)加了一个\*
  • 2.3 net.train()/ net.eval()
  • 2.4 用到的argsparse模块
  • 2.5 创建记录数据的txt文件
  • 2.6 sum_loss 、 predicted 、total 、correct (重点理解)
  • 2.7 __init__中使用nn.Relu(),forward中使用F.relu(),两者的区别?
  • 3 源代码附详细解释

      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下表现应该比较正常,故这里做了些调整。

resnet维度变化 resnet18模型大小_卷积神经网络

# 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)