ResNet34基础知识及实现Cifar-10分类(pytorch)

主要贡献:

  1. 网络变深以后的梯度消失,梯度爆炸问题,这个问题被BN解决。
  2. 网络退化问题,并不是过拟合,而是在增加更多的层后导致的训练误差。

如relu函数,低维度的特征通过relu后,会有一部分被毁掉,因为维度越低分布到relu激活带的可能性就越小。那么在反向传播的时候就会出现梯度消失,那么神经元的权重就无法更新,导致特征退化。那么理想解决办法就是对冗余数据使用relu,对不含冗余信息的使用线性激活。

对现有网络A增加恒等映射为更深的B,A和B的效果应该是一样的,所以可以证明增加深度不会导致网络的效果更差。

残差块

ResNet34基础知识及实现Cifar-10分类(pytorch)_第1张图片

  • 若没有右侧箭头则为最原始的:F′(x,w′)→H(x),找出w'使得F'函数逼近真实函数。
  • 加入右侧箭头(Shortcut Connection)变成F(x,w)+ x → H(x)。如果w为0,那么就是恒等映射。同时此时如果H(x)产生小的变化,F比F'的变化更明显对权重的调整影响就更大,反向传播的梯度更大,调整作用更大。
  • ResNet34基础知识及实现Cifar-10分类(pytorch)_第2张图片第一个因子,为损失函数到L的梯度,而最后一项残差梯度需要经过每个带权重的层,有1的存在不会导致梯度消失。

block结构

ResNet34基础知识及实现Cifar-10分类(pytorch)_第3张图片

block存在两种形式,左边适用于较浅的网络,用在ResNet34中,右图用在网络较深的时候,在ResNet50/101/152中,目的是通过1*1卷积该改变维度进而降低参数量。

ResNet34基础知识及实现Cifar-10分类(pytorch)_第4张图片

网络结构图中存在部分虚线,虚线是因为feature map数量发生了变化。在ShortCut connection中加1*1的卷积使维度统一。ResNet34基础知识及实现Cifar-10分类(pytorch)_第5张图片

ResNet34基础知识及实现Cifar-10分类(pytorch)_第6张图片

  • 与VGGNet不同的是,ResNet除了一开始的3*3的最大池化层,其他下采样都采用的stride=2的卷积层实现。
  • 当feature map的大小降低一半的时候,数量增加一倍。
  • Global Avg Pool
    • 局部池化主要是为了增大卷积核的感受野,最大池化保留纹理信息,平均池化保留总体信息。
    • 全局池化,每个特征图被压缩到一个点,更注重通道间的关系。GAP更重视整体信息,GMP容易受极值影响。(一个物体占feature map的尺寸很大的时候,GAP更能描述特征图的相应权重。
      但是放在目标跟踪方面,比如你要跟踪的是一个比较小的物体,特征图相应很大,但范围很小。而在画面上有一个比较大的不相关的物体,特征图相应虽然小,但是占的范围大。这个时候可能GAP算出来反而是大不相关的物体权重(attention)更高,这样响应极值的GMP可能更好。)
    • ResNet34基础知识及实现Cifar-10分类(pytorch)_第7张图片

    import torch
    from torch import nn
    from torch.nn import functional as F
    
    
    # 残差模块
    class ResidualBlock(nn.Module):
        def __init__(self, in_ch, out_ch, stride=1, shortcut=None):
            super(ResidualBlock, self).__init__()
            self.left = nn.Sequential(
                nn.Conv2d(in_ch, out_ch, kernel_size=3, stride=stride, padding=1, bias=False),
                nn.BatchNorm2d(out_ch),
                nn.ReLU(inplace=True),
    
                nn.Conv2d(out_ch, out_ch, kernel_size=3, stride=1, padding=1, bias=False),
                nn.BatchNorm2d(out_ch),
            )
            self.right = shortcut
    
        def forward(self, x):
            output = self.left(x)
            # 对应网络中的实线和虚线,是否需要调整特征图维度
            residual = x if self.right is None else self.right(x)
            output += residual
            return F.relu(output)
    
    
    def _make_layer(in_ch, out_ch, block_num, stride=1):
        # 维度增加时执行网络中的虚线,对shortcut 使用1*1矩阵增大维度
        shortcut = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 1, stride, bias=False),
            nn.BatchNorm2d(out_ch),
        )
        layers = []
        layers.append(ResidualBlock(in_ch, out_ch, stride, shortcut))
        # 只有第一个shortcut需要统一特征图维度。
    
        for i in range(1, block_num):
            layers.append(ResidualBlock(out_ch, out_ch))
        return nn.Sequential(*layers)
    
    
    class ResNet34(nn.Module):
        def __init__(self, num_classes=10):
            super(ResNet34, self).__init__()
            # 第一层,输入为224*224*3。
            self.pre = nn.Sequential(
                # (224+p*2-7)/s+1=(224+6-7)/2+1=112
                nn.Conv2d(3, 64, kernel_size=7, padding=3, stride=2, bias=False),
                nn.BatchNorm2d(64),
                nn.ReLU(inplace=True),
                # 输入为112*112*64,输出为56*56*64
                nn.MaxPool2d(kernel_size=3, padding=1, stride=2),
            )
    
            # 四个layer分别有3,4,6,3个residual block
            self.layer1 = _make_layer(64, 64, 3)  # 56*56*64
            self.layer2 = _make_layer(64, 128, 4, stride=2)  # 28*28*128 stride=2在第一层实现下采样
            self.layer3 = _make_layer(128, 256, 6, stride=2)  # 14*14*256
            self.layer4 = _make_layer(256, 512, 3, stride=2)  # 7*7*512
            # 最终的全连接层
            self.Conv = nn.Sequential(
                self.layer1,
                self.layer2,
                self.layer3,
                self.layer4,
            )
            self.fc = nn.Linear(512, num_classes)  # 7*7*512使用全局平均池化
    
        def forward(self, x):
            x = self.pre(x)
            x = self.Conv(x)
            x = F.avg_pool2d(x, 7)
            x = x.view(x.size(0), -1)
            x = self.fc(x)
    
            return x
    
    
    if __name__ == "__main__":
        x = torch.rand([2, 3, 224, 224])
        model = ResNet34()
        print(model)
        y = model(x)
        print(y)
    

    因为服务器中存在Cifar10数据,所以这部分download设为False,如果需要下载则设置为True。

  • # 下载CIFAR-10数据集
    train_dataset = datasets.CIFAR10(root='./data', train=True, transform=data_transform, download=False)
    train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset,
                                                   batch_size=64,
                                                   shuffle=True)
    test_dataset = datasets.CIFAR10('./data', train=False, transform=data_transform, download=False)
    test_dataloader = torch.utils.data.DataLoader(test_dataset,
                                                  batch_size=64,
                                                  shuffle=False)
    import os
    
    import torch
    from torch import nn
    from model import ResNet34
    from torch import optim
    from torchvision import datasets, transforms
    import torch.utils.data
    
    learning_rate = 1e-3
    num_epoches = 100
    
    data_transform = transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize(mean = [0.485, 0.456, 0.406],
                             std = [0.229, 0.224, 0.225])
    ])
    
    # 下载CIFAR-10数据集
    train_dataset = datasets.CIFAR10(root='./data', train=True, transform=data_transform, download=False)
    train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset,
                                                   batch_size=64,
                                                   shuffle=True)
    test_dataset = datasets.CIFAR10('./data', train=False, transform=data_transform, download=False)
    test_dataloader = torch.utils.data.DataLoader(test_dataset,
                                                  batch_size=64,
                                                  shuffle=False)
    
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model = ResNet34().to(device)
    
    # loss和optimizer
    loss_fn = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)
    lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
    
    
    def train(dataloader, model, loss_fn, optimizer):
        loss, current, n = 0.0, 0.0, 0
        for batch, (x, y) in enumerate(dataloader):
            # 前向传播
            x, y = x.to(device), y.to(device)
            output = model(x)
            cur_loss = loss_fn(output, y)
            _, pred = torch.max(output, axis=1)
            cur_acc = torch.sum(y==pred)/output.shape[0]
    
            # 反向传播
            # 清楚过往梯度
            optimizer.zero_grad()
            cur_loss.backward()
            # 根据梯度更新网络参数
            optimizer.step()
            loss += cur_loss.item()
            current += cur_acc.item()
            n += 1
    
        train_loss = loss / n
        train_acc = current / n
    
        # 计算训练的错误率
        print('train_loss:' + str(train_loss))
        # 计算训练的准确率
        print('train_acc:' + str(train_acc))
    
    
    def val(dataloader, model, loss_fn):
        model.eval()
        loss, current, n = 0.0, 0.0, 0
        # with torch.no_grad():将with语句包裹起来的部分停止梯度的更新,从而节省了GPU算力和显存,但是并不会影响dropout和BN层的行为
        with torch.no_grad():
            for batch, (x, y) in enumerate(dataloader):
                # 前向传播
                x, y = x.to(device), y.to(device)
                output = model(x)
                cur_loss = loss_fn(output, y)
                _, pred = torch.max(output, axis=1)
                cur_acc = torch.sum(y == pred) / output.shape[0]
    
                loss += cur_loss.item()
                current += cur_acc.item()
                n += 1
    
            # 验证的错误率
            print("val_loss: " + str(loss / n))
            print("val_acc: " + str(current / n))
            # 返回模型准确率
            return current / n
    
    min_acc = 0
    for t in range(num_epoches):
        print(f'epoch {t + 1}\n-----------------')
        train(train_dataloader, model, loss_fn, optimizer)
        a = val(test_dataloader, model, loss_fn)
        if a > min_acc:
            folder = 'save_model'
            if not os.path.exists(folder):
                os.mkdir('save_model')
            min_acc = a
            print('save best model')
    
            torch.save(model.state_dict(), 'save_model/best_model.pth')
    
    print('Done!')
    

     

你可能感兴趣的:(分类,pytorch,深度学习)