인공지능/Deep Learning

[DeepLearning] Dive into Deep Learning 필사 7. Convolutional Neural Networks (CNN) (8.6. Residual Networks (ResNet) and ResNeXt)

이준언 2024. 10. 11. 16:50

8.6. Residual Networks (ResNet) and ResNeXt

신경망의 성능을 높이기 위해 네트워크를 깊게 만드려는 시도가 많았지만, 네트워크가 너무 깊어지면 오히려 성능이 떨어지는 기울기 소실 또는 기울기 폭발 문제가 발생. 이를 해결하기 위해 residual learning을 도입한 ResNet 등장

 

* ResNet (Residual Networks)

목표: 매우 깊은 신경망에서 발생하는 기울기 소실(vanishing gradient) 문제를 해결하는 것

핵심 개념: 잔차 블록 (Residual Block)

ResNet의 잔차 블록은 입력을 변화하는 대신, 입력과 변환값을 더하는 방식 사용. 이 때문에 네트워크가 더 깊어지더라도 성능 저하 없이 학습 가능

 

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

8.6.1. Function Classes

 

8.6.2. Residual Blocks

class Residual(nn.Module):  #@save
    """The Residual block of ResNet models."""
    def __init__(self, num_channels, use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1,
                                   stride=strides)
        self.conv2 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.LazyConv2d(num_channels, kernel_size=1,
                                       stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.LazyBatchNorm2d()
        self.bn2 = nn.LazyBatchNorm2d()

    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)
blk = Residual(3)
X = torch.randn(4, 3, 6, 6)
blk(X).shape
blk = Residual(6, use_1x1conv=True, strides=2)
blk(X).shape

 

 

 

8.6.3. ResNet Model

class ResNet(d2l.Classifier):
    def b1(self):
        return nn.Sequential(
            nn.LazyConv2d(64, kernel_size=7, stride=2, padding=3),
            nn.LazyBatchNorm2d(), nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
@d2l.add_to_class(ResNet)
def block(self, num_residuals, num_channels, first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(num_channels, use_1x1conv=True, strides=2))
        else:
            blk.append(Residual(num_channels))
    return nn.Sequential(*blk)
@d2l.add_to_class(ResNet)
def __init__(self, arch, lr=0.1, num_classes=10):
    super(ResNet, self).__init__()
    self.save_hyperparameters()
    self.net = nn.Sequential(self.b1())
    for i, b in enumerate(arch):
        self.net.add_module(f'b{i+2}', self.block(*b, first_block=(i==0)))
    self.net.add_module('last', nn.Sequential(
        nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(),
        nn.LazyLinear(num_classes)))
    self.net.apply(d2l.init_cnn)
class ResNet18(ResNet):
    def __init__(self, lr=0.1, num_classes=10):
        super().__init__(((2, 64), (2, 128), (2, 256), (2, 512)),
                       lr, num_classes)

ResNet18().layer_summary((1, 1, 96, 96))

 

 

8.6.4. Training

model = ResNet18(lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(96, 96))
model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn)
trainer.fit(model, data)

 

 

 

 

1. 

Inception Block: 여러 크기의 필터를 병렬로 사용하는 방식. 모델이 여러 수준의 특징을 동시에 학습. 병렬로 다양한 필터를 사용하기 때문에 계산량이 비교적 많아짐

계산량 많음

복잡한 특징 추출에 유리

넓은 범위의 함수 클래스에서 학습 가능

 

Residual Block: 입력을 그대로 출력에 전달하는 skip connection을 추가하여 기울기 소실 문제를 해결. 이 구조는 매우 깊은 네트워크에서도 성능 저하 없이 학습 가능. 

계산비용이 상대적으로 적음

깊은 네트워크에서 성능 유지에 유리

복잡한 함수 설명에 용이하지만 필터 스케일이 고정적

 

2. 

Layer 수를 조절하여 다양한 변형 네트워크 구현 가능

 

3. 

Bottleneck 구조는 깊은 네트워크에서 사용됨. 이 구조는 1x1 합성곱을 사용하여 채널 수를 줄인 후, 3x3 합성곱을 적용하고, 다시 1x1 합성곱을 통해 원래 채널 수로 복원하는 방식. 이를 통해 계산 비용을 줄이면서 깊은 네트워크 구현 가능

 

4. 

ResNet 논문의 최신 버전에서는 블록 내 연산 순서를

Convolution > Batch > Normalization > Activation 에서

Batch Normalization > Activation > Convolution 으로 변경

class NewResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(NewResidualBlock, self).__init__()
        self.bn1 = nn.BatchNorm2d(in_channels)
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        self.shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride) if in_channels != out_channels else nn.Identity()

    def forward(self, x):
        out = F.relu(self.bn1(x))
        out = self.conv1(out)
        out = F.relu(self.bn2(out))
        out = self.conv2(out)
        out += self.shortcut(x)
        return F.relu(out)

 

5. 

함수의 클래스가 중첩된 경우, 이론적으로 더 큰 함수 클래스는 더 작은 클래스의 함수를 포함해야함. 하지만 함수의 복잡도를 계속해서 증가시키는 것이 항상 더 나은 결과를 보장하는 것은 아님

 

과적합, 최적화의 어려움, 계산 비용 과다 등의 이유로 무조건 복잡성을 늘리기보다는 적절한 복잡도를 유지하는 것이 중요