【OUCディープラーニング入門】第3週学習記録:ResNet+ResNeXt

Part1 レスネット

1 論文の読書

ネットワーク層の数が深くなるほど学習が難しくなることが知られている. この問題を解決するために, 本稿では, 最適化が容易であり, 深さが深くなるにつれて高精度を達成できる残差学習モジュールを提案する.とても深いです。

深いネットワークの学習では主に 2 つの問題に直面します。1 つは勾配消失/勾配爆発、もう 1 つはネットワークの劣化です。前者は操作の正規化、オプティマイザー、学習率の調整によって解決でき、後者はネットワークの層を深くすることで解決できます。トレーニング効果は の時点で飽和に達し、このネットワークの劣化は過学習によって引き起こされるものではありません。

したがって、この論文では、恒等変換から始めて、この深層ネットワークの下でのネットワーク劣化問題を解決するための残差学習モジュールを提案します。これにより、深層ネットワークの構造モジュールは、より良いトレーニング結果を得るために非線形変換を実行できるだけでなく、実行も実行できます。現在のトレーニング効果を十分に維持するためのアイデンティティ変換。

ResNet には主に次の特徴があります。

  1. 残差表現
  2. ショートカット接続(アイデンティティ変換を実現)

2 ネットワーク構造

ネットワークのハイライト:

  • 深いネットワーク構造(1000突破)
  • 残りのモジュール
  • ドロップアウトを破棄し、バッチ正規化を使用してトレーニングを高速化します

 従来のディープネットワーク構造が直面する問題:

  • グラデーションの消失または爆発
  • 劣化の問題

残留構造:

左側の残差構造は浅いネットワーク (ResNet34) に使用され、右側の BottleNeck は ResNet50/101/152 に使用されます。メイン ブランチとショートカットの出力特徴行列の形状は同じである必要があります。

BottleNeck の 1*1 コンボリューション カーネルは次元を減らすために使用され、最初に次元を減らし、次に次元を増やします。これはパラメータの量を減らすのに有益です。

ResNet 構造の概略図の点線のショートカットは、ここの残差構造に次元拡張があり、次元拡張用の 1*1 コンボリューション カーネルが対応するショートカットに追加されていることを示します。

元の論文の ResNet は、PyTorch によって実装された公式の ResNet とは異なります。元の論文の点線残差構造の主分岐では、1*1 畳み込みのステップ サイズは 2、3*3 畳み込みのステップ サイズは 2 です。は 1; 公式の PyTorch の実装では、1*1 畳み込みのステップ サイズは 1、3*3 畳み込みのステップ サイズは 2 であり、精度率がわずかに向上します。

バッチ正規化 (BN) : 平均値 0、分散 1 の分布法則を満たすようにバッチの特徴マップを調整します。平均値と分散は両方ともベクトルであり、次元と深さは順方向の統計に対応します。 γ と β は、伝播中に逆にトレーニングされます。(バッチ正規化とpytorch実験の詳しい説明_Sunflower's Mung Bean Blog-CSDN Blog_batchnormalization pytorch )

 

 BN を使用する場合は、次の点に注意してください

  • トレーニング中は training=True、検証中は training=False。これは、pytorch でモデルを作成する model.train() メソッドと model.eval() メソッドによって制御できます。
  • バッチ サイズを大きく設定すると、平均と分散がトレーニング セット全体の平均と分散に近づきます。
  • bn 層を畳み込み層 (Conv) と活性化層 (Relu など) の間に配置することをお勧めします。畳み込み層はバイアスを使用しないでください。

転移学習の利点: より良い結果を迅速にトレーニングでき、データセットが小さい場合でも、望ましい効果を達成できます (トレーニング前の処理方法に注意してください)

転移学習の一般的な方法: 重みをロードした後にすべてのパラメーターをトレーニングする; 重みをロードした後にパラメーターの最後の数層のみをトレーニングする; 重みをロードした後に全結合層を追加する

3 PyTorch に基づいて ResNet を構築する

コードリンク: (colab) PyTorch に基づいて ResNet を構築する

from torch.nn.modules.batchnorm import BatchNorm2d
import torch
import torch.nn as nn


# 浅层ResNet的残差结构
class BasicBlock(nn.Module):
  expansion = 1 # 主分支中卷积核个数是否发生变化
  def __init__(self,in_channel,out_channel,stride=1,downsample=None):
    super(BasicBlock,self).__init__()
    self.conv1 = nn.Conv2d(in_channels=in_channel,out_channels=out_channel,
                kernel_size=3,stride=stride,padding=1,bias=False)
    self.bn1 = nn.BatchNorm2d(out_channel)
    self.relu = nn.ReLU()
    self.conv2 = nn.Conv2d(in_channels=out_channel,out_channels=out_channel,
                kernel_size=3,stride=1,padding=1,bias=False)
    self.bn2 = nn.BatchNorm2d(out_channel)
    self.downsample = downsample

  def forward(self,x):
    identity = x
    # 如果传入了下采样,就是虚线的残差块
    if self.downsample is not None:
      identity = self.downsample(x)

    out = self.conv1(x)
    out = self.bn1(out)
    out = self.relu(out)

    out = self.conv2(out)
    out = self.bn2(out)

    out += identity
    out = self.relu(out)

    return out


# 深层ResNet的残差结构
class Bottleneck(nn.Moudule):
  expansion = 4
  def __init__(self,in_channel,out_channel,stride=1,downsample=None):
    super(Bottleneck,self).__init__()
    # 降维
    self.conv1 = nn.Conv2d(in_channels=in_channel,out_channels=out_channel,
                kernel_size=1,stride=1,bias=False)
    self.bn1 = nn.BatchNorm2d(out_channel)

    self.conv2 = nn.Conv2d(in_channels=out_channel,out_channels=out_channel,
                kernel_size=3,stride=stride,padding=1,bias=False)
    self.bn2 = nn.BatchNorm2d(out_channel)

    # 升维
    self.conv3 = nn.Conv2d(in_channels=out_channel,out_channels=out_channel*self.expansion,
                kernel_size=1,stride=1,bias=False)
    self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)

    self.relu = nn.ReLU(inplace=True)
    self.downsample = downsample

  def forward(self,x):
    identity = x
    # 如果传入了下采样,就是虚线的残差块
    if self.downsample is not None:
      identity = self.downsample(x)

    out = self.conv1(x)
    out = self.bn1(out)
    out = self.relu(out)

    out = self.conv2(out)
    out = self.bn2(out)
    out = self.relu(out)

    out = self.conv3(out)
    out = self.bn3(out)

    out += identity
    out = self.relu(out)

    return out



# ResNet
class ResNet(nn.Module):
  def __init__(self,block,blocks_num,num_classes=1000,include_top=True):
    super(ResNet,self).__init__()
    self.include_top = include_top
    self.in_channel = 64

    self.conv1 = nn.Conv2d(3,self.in_channel,kernel_size=7,stride=2,padding=3,bias=False)
    self.bn1 = nn.BatchNorm2d(self.in_channel)
    self.relu = nn.ReLU(inplace=True)
    self.maxpool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1)

    self.layer1 = self._make_layer(block,64,blocks_num[0])
    self.layer2 = self._make_layer(block,128,blocks_num[1],stride=2)
    self.layer3 = self._make_layer(block,256,blocks_num[2],stride=2)
    self.layer4 = self._make_layer(block,512,blocks_num[3],stride=2)

    # 如果包含全连接层
    if self.include_top:
      self.avgpool = nn.AdaptiveAvgPool2d((1,1))
      self.fc = nn.Linear(512*block.expansion,num_classes)

    for m in self.modules():
      if isinstance(m,nn.Conv2d):
        nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')

  def _make_layer(self,block,channel,block_num,stride=1):
    downsample = None
    if stride!=1 or self.in_channel!=channel*block.expansion:
      downsample = nn.Sequential(
          nn.Conv2d(self.in_channel,channel*block.expansion,kernel_size=1,stride=stride,bias=False),
          nn.BatchNorm2d(channel*block.expansion)
      )
      layers = []
      layers.append(block(self.in_channel,channel,downsample=downsample,stride=stride))
      self.in_channel = channel*block.expansion

    for _ in range(1,block_num):
      layers.append(block(self.in_channel,channel))

    return nn.Sequential(*layers) # 非关键字参数

  def forward(self,x):
    x = self.conv1(x)
    x = self.bn1(x)
    x = self.relu(x)
    x = self.maxpool(x)

    x = self.layer1(x)
    x = self.layer2(x)
    x = self.layer3(x)
    x = self.layer4(x)

    if self.include_top:
      x = self.avgpool(x)
      x = torch.flatten(x,1)
      x = self.fc(x)

    return x


def resnet18(num_classes=1000,include_top=True):
  return ResNet(BasicBlock,[2,2,2,2],num_classes=num_classes,include_top=include_top)

def resnet34(num_classes=1000,include_top=True):
  return ResNet(BasicBlock,[3,4,6,3],num_classes=num_classes,include_top=include_top)

def resnet50(num_classes=1000,include_top=True):
  return ResNet(Bottleneck,[3,4,6,3],num_classes=num_classes,include_top=include_top)

def resnet101(num_classes=1000,include_top=True):
  return ResNet(Bottleneck,[3,4,23,3],num_classes=num_classes,include_top=include_top)

def resnet152(num_classes=1000,include_top=True):
  return ResNet(Bottleneck,[3,8,36,3],num_classes=num_classes,include_top=include_top)

Part2 レスネクスト

1 論文の読書

この論文は、ResNet に基づいて「カーディナリティ (変換セットのサイズ)」という概念を提案し、「カーディナリティ」がネットワークの深さと幅以外にトレーニング効果に影響を与えるもう 1 つの重要な要素であり、「カーディナリティ」が増加することを指摘しました。この操作はネットワークの深さと幅を増やすよりも効率的であり、ハイパーパラメータのサイズを大きくしてもトレーニング効果が必ずしも向上するとは限らないため、ハイパーパラメータのサイズが大きくなるとトレーニングの難易度や不確実性が増加します。 , ネットワーク構造を賢く設計すると、既存のネットワークの深さを単純に増やすよりも優れた結果を達成できます。

この観点から、この論文は ResNet のネットワーク構造を改善し、同じ構造の複数の畳み込みを残差ブロックとして積み重ね、この構造により過学習問題をよりよく回避できると考えています。残差ブロックの入力は同じサイズのいくつかの部分に分割され、各部分に対して同じ畳み込み演算が実行され、結果が統合されて出力が形成されます。インセプション構造と比較して、この構造モジュールはより簡潔で実装が簡単です。

実験の結果、101 層の ResNeXt は 200 層の ResNet よりも正確であり、複雑さは後者の半分にすぎないことがわかりました。ResNeXt の主な機能は次のとおりです。

  • マルチブランチ畳み込みネットワーク
  • グループ畳み込み
  • 圧縮畳み込みネットワーク
  • 加算変換

ResNeXt の残差ブロック構造を構築するには、次の 2 つのガイドラインに従う必要があります。

  1. 同じサイズの空間マップはハイパーパラメータを共有します
  2. 空間マップが 2 倍にダウンサンプリングされるたびに、ブロックの幅も 2 倍になります。

2 ネットワーク構造

ResNetと比較して、ResNeXtはResNetをベースにブロック構造を改善し、バックボーンをグループ畳み込みに変更することで、同じ計算量の下では、ResNeXtの方が誤り率が低くなります。

 グループ畳み込みは、従来の畳み込みよりもパラメーターが少なく、出力次元が入力次元と同じである場合、畳み込み用の入力特徴行列の各チャネルにチャネル 1 の畳み込みカーネルを割り当てることと同じになります。

次の 3 つの形式は、計算において完全に同等です。

 ResNet50 および ResNeXt50:

ブロック層の数が 3 以上の場合にのみ、意味のあるグループ コンボリューション ブロックを構築できるため、この改善は浅い ResNet にはほとんど影響しません。

3 PyTorch に基づいて ResNet を構築する

 コードリンク: (colab) PyTorch に基づいて ResNeXt をビルドする

# 基于PyTorch搭建ResNeXt

from torch.nn.modules.batchnorm import BatchNorm2d
import torch
import torch.nn as nn


# 浅层的残差结构(不变)
class BasicBlock(nn.Module):
  expansion = 1 # 主分支中卷积核个数是否发生变化
  def __init__(self,in_channel,out_channel,stride=1,downsample=None):
    super(BasicBlock,self).__init__()
    self.conv1 = nn.Conv2d(in_channels=in_channel,out_channels=out_channel,
                kernel_size=3,stride=stride,padding=1,bias=False)
    self.bn1 = nn.BatchNorm2d(out_channel)
    self.relu = nn.ReLU()
    self.conv2 = nn.Conv2d(in_channels=out_channel,out_channels=out_channel,
                kernel_size=3,stride=1,padding=1,bias=False)
    self.bn2 = nn.BatchNorm2d(out_channel)
    self.downsample = downsample

  def forward(self,x):
    identity = x
    # 如果传入了下采样,就是虚线的残差块
    if self.downsample is not None:
      identity = self.downsample(x)

    out = self.conv1(x)
    out = self.bn1(out)
    out = self.relu(out)

    out = self.conv2(out)
    out = self.bn2(out)

    out += identity
    out = self.relu(out)

    return out


# 深层的残差结构
class Bottleneck(nn.Moudule):
  expansion = 4
  def __init__(self,in_channel,out_channel,stride=1,downsample=None,groups=1,width_per_group=64):
    super(Bottleneck,self).__init__()

    width = int(out_channel*(width_per_group/64.))*groups

    # 降维
    self.conv1 = nn.Conv2d(in_channels=in_channel,out_channels=width,
                kernel_size=1,stride=1,bias=False)
    self.bn1 = nn.BatchNorm2d(out_channel)

    self.conv2 = nn.Conv2d(in_channels=width,out_channels=width,groups=groups,
                kernel_size=3,stride=stride,padding=1,bias=False)
    self.bn2 = nn.BatchNorm2d(width)

    # 升维
    self.conv3 = nn.Conv2d(in_channels=width,out_channels=out_channel*self.expansion,
                kernel_size=1,stride=1,bias=False)
    self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)

    self.relu = nn.ReLU(inplace=True)
    self.downsample = downsample

  def forward(self,x):
    identity = x
    # 如果传入了下采样,就是虚线的残差块
    if self.downsample is not None:
      identity = self.downsample(x)

    out = self.conv1(x)
    out = self.bn1(out)
    out = self.relu(out)

    out = self.conv2(out)
    out = self.bn2(out)
    out = self.relu(out)

    out = self.conv3(out)
    out = self.bn3(out)

    out += identity
    out = self.relu(out)

    return out



# ResNet
class ResNeXt(nn.Module):
  def __init__(self,block,blocks_num,num_classes=1000,include_top=True):
    super(ResNeXt,self).__init__()
    self.include_top = include_top
    self.in_channel = 64

    self.conv1 = nn.Conv2d(3,self.in_channel,kernel_size=7,stride=2,padding=3,bias=False)
    self.bn1 = nn.BatchNorm2d(self.in_channel)
    self.relu = nn.ReLU(inplace=True)
    self.maxpool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1)

    self.layer1 = self._make_layer(block,64,blocks_num[0])
    self.layer2 = self._make_layer(block,128,blocks_num[1],stride=2)
    self.layer3 = self._make_layer(block,256,blocks_num[2],stride=2)
    self.layer4 = self._make_layer(block,512,blocks_num[3],stride=2)

    # 如果包含全连接层
    if self.include_top:
      self.avgpool = nn.AdaptiveAvgPool2d((1,1))
      self.fc = nn.Linear(512*block.expansion,num_classes)

    for m in self.modules():
      if isinstance(m,nn.Conv2d):
        nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')

  def _make_layer(self,block,channel,block_num,stride=1):
    downsample = None
    if stride!=1 or self.in_channel!=channel*block.expansion:
      downsample = nn.Sequential(
          nn.Conv2d(self.in_channel,channel*block.expansion,kernel_size=1,stride=stride,bias=False),
          nn.BatchNorm2d(channel*block.expansion)
      )
      layers = []
      layers.append(block(self.in_channel,channel,downsample=downsample,
                stride=stride,groups=self.groups,width_per_group=self.width_per_group))
      self.in_channel = channel*block.expansion

    for _ in range(1,block_num):
      layers.append(block(self.in_channel,channel,groups=self.groups,
                width_per_group=self.width_per_group))

    return nn.Sequential(*layers) # 非关键字参数

  def forward(self,x):
    x = self.conv1(x)
    x = self.bn1(x)
    x = self.relu(x)
    x = self.maxpool(x)

    x = self.layer1(x)
    x = self.layer2(x)
    x = self.layer3(x)
    x = self.layer4(x)

    if self.include_top:
      x = self.avgpool(x)
      x = torch.flatten(x,1)
      x = self.fc(x)

    return x


def resnet18(num_classes=1000,include_top=True):
  return ResNet(BasicBlock,[2,2,2,2],num_classes=num_classes,include_top=include_top)

def resnet34(num_classes=1000,include_top=True):
  return ResNet(BasicBlock,[3,4,6,3],num_classes=num_classes,include_top=include_top)

def resnet50(num_classes=1000,include_top=True):
  return ResNet(Bottleneck,[3,4,6,3],num_classes=num_classes,include_top=include_top)

def resnet101(num_classes=1000,include_top=True):
  return ResNet(Bottleneck,[3,4,23,3],num_classes=num_classes,include_top=include_top)

def resnet152(num_classes=1000,include_top=True):
  return ResNet(Bottleneck,[3,8,36,3],num_classes=num_classes,include_top=include_top)


def resnext50_32_4d(num_classes=1000,include_top=True):
  groups = 32
  width_per_group = 4
  return ResNeXt(Bottleneck,[3,4,6,3],num_classes=num_classes,include_top=include_top,
                groups=groups,width_per_group=width_per_group)
  
def resnext101_32_8d(num_classes=1000,include_top=True):
  groups = 32
  width_per_group = 8
  return ResNeXt(Bottleneck,[3,4,23,3],num_classes=num_classes,include_top=include_top,
                groups=groups,width_per_group=width_per_group)

Part3 コード演習: 犬猫戦争

コードリンク: (colab) 犬猫戦争

データローダー:

data_dir = '/content/drive/MyDrive/Colab Notebooks/cat_dog/'
train_dir = data_dir+'train/'
test_dir = data_dir+'test/'
val_dir = data_dir+'val/'

train_imgs = os.listdir(train_dir)
train_labels = []


normalize = transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])

transform = transforms.Compose([transforms.Resize([32,32]),transforms.ToTensor(),normalize])

class CatDogDataset(Dataset):
  def __init__(self, root, transform=None):
    self.root = root
    self.transform = transform
    # 正样本
    self.imgs = os.listdir(self.root)
    self.labels = []
    
    for img in self.imgs:
      if img.split('_')[0]=='cat':
        self.labels.append(0)
      if img.split('_')[0]=='dog':
        self.labels.append(1)

  def __len__(self):
    return len(self.imgs)

  def __getitem__(self, index):
    label = self.labels[index]
    img_dir = self.root + str(self.imgs[index])
    img = Image.open(img_dir)

    # transform?
    if self.transform is not None:
      img = self.transform(img)

    return img,torch.from_numpy(np.array(label))  # 返回数据+标签

ルネット5:

class LeNet5(nn.Module):
    def __init__(self): 
        super(LeNet5, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, 5)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(16, 32, 5)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(32*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)
                            
    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.pool1(x)
        x = F.relu(self.conv2(x))
        x = self.pool2(x)
        x = x.view(-1, 32*5*5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

関連パラメータ:

トレーニング (左側が ResNet34、右側が LeNet5):

 

 

結果の CSV ファイルを生成します。

採点結果: 

 

同じ最適化パラメーターとトレーニング ラウンド数を使用した場合、ResNet34 が LeNet5 よりも優れていることがわかります。

Part4 思考に関する質問

1. 残留学習

基本マッピングが h(x) で、残差ブロックの 2 つのブランチのマッピングがそれぞれ f(x) と x であると仮定すると、h(x)=x+f(x)、f(x) がメインになります。この構造は、アイデンティティ マッピングの実現と前処理に役立ち、ネットワークの劣化の問題を軽減します。勾配を導出する場合、h'(x)=1+f'(x) があり、これにより勾配消失の問題も軽減できます。

2. バッチ正規化の原理

畳み込みニューラルネットワークには多数の隠れ層が含まれており、学習により各層のパラメータが変化するため、隠れ層の入力分布が常に変化するため、学習速度が低下し、活性化関数の勾配が飽和してしまいます。この問題を解決するアイデアとして正規化がありますが、正規化を多用すると学習過程の計算が非常に複雑になり、小さすぎると効果がなくなるため、BNが提案されています。まずデータを複数のバッチに分割し、次に各バッチで正規化操作を実行します。

3. グループ畳み込みによって精度が向上するのはなぜですか? グループ畳み込みによって精度が向上し、計算量が削減できるのであれば、スコアの数はできるだけ多くできないでしょうか。

ResNeXtの論文の内容によると、グループ畳み込みの本来の目的は、複数のGPUで同時にモデルをトレーニングすることを容易にすることであり、グループ畳み込みは計算量を削減できるが、グループ畳み込みが効果を発揮するという証拠はほとんどない、と指摘している。現在でも、現時点では関連する証拠は見つかっていません。ResNet から ResNeXt への改良は主にカーディナリティの概念を提案したものであり、個人的にはグループ畳み込みで精度が向上するのであれば、それはグループ畳み込みが畳み込みニューラルネットワークと同様にグループ学習を行うことでより深い情報を学習できるからではないかと考えています。完全に接続された層。

グループ畳み込みのグループ数は多すぎてはなりません。グループが多すぎると、特徴抽出が非常に断片化され、主要な特徴の抽出に役立ちません。

おすすめ

転載: blog.csdn.net/qq_55708326/article/details/125957382