기존에 공부했던 코드라 따로 출처를 파악하기 힘들어서 기재하지 못하는데, 나중에 알게 되면 코드 출처도 올리겠습니다. 또한 위 코드를 바탕으로 train과 main 파일도 만들어서 깃허브에 업로드했습니다.
이번 코드 리뷰에서는 PyTorch로 구현한 resnet.py 코드에 대해 구조적으로 분석하고, 핵심 설계 동작에 대해서 살펴보고자 한다. 코드는 ResNet18부터 ResNet152까지의 다양한 모델을 모듈화된 방식으로 생성할 수 있는 코드이다.
ResNet 코드에 대해서 설명하기에 앞서 조금 기본적인 내용들에 대해서 말해보겠습니다.
Class :
• 딥러닝 모델을 하나의 객체로 정의하는 방법
• PyTorch에서는 반드시 nn.Module을 상속해서 정의합니다.
• 모델 내 layer, 연산 순서를 정의하며 재사용과 관리가 용이합니다.
super().init() :
• 상속받은 nn.Module 클래스의 초기화 함수를 명시적으로 호출합니다.
• PyTorch 내부 기능 (예: .parameters(), .to(), .eval())을 제대로 사용하기 위해 반드시 필요합니다.
init() :
• 클래스가 처음 생성될 때 호출되는 초기화 함수입니다.
• 여기에 네트워크에서 사용할 레이어를 정의합니다.
• self.conv, self.fc, self.relu 등은 전부 모델의 구성 요소(Layer)입니다.
forward() :
• 모델의 실제 연산 흐름을 정의하는 함수입니다.
• 이 안에서 init에 정의한 레이어들을 호출해서 데이터를 순차적으로 변환합니다.
• PyTorch는 모델 호출 시 자동으로 forward()를 실행합니다.
nn. Sequential() :
• 여러 레이어를 하나의 블록처럼 묶어주는 도구입니다.
• 순차적으로 실행되며, 코드를 간결하게 정리할 수 있습니다.
• 블록처럼 구성해서 self.block(x) 형태로 한 번에 호출 가능합니다.
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1) :
• 2D 이미지에 적용되는 합성곱 레이어
• 특징 추출기 역할을 하며 필터를 통해 공간 정보를 학습합니다.
• 주요 파라미터:
o in_channels: 입력 채널 수 (예: RGB 이미지 → 3)
o out_channels: 필터 개수 (출력 채널 수)
o kernel_size: 필터 크기
o stride, padding: 이동 간격 및 테두리 처리.
nn.BatchNorm2d(64) :
• 배치 정규화 레이어로 학습을 안정화시킵니다.
• 입력 feature map의 분포를 평균 0, 분산 1로 정규화합니다.
• 일반적으로 Conv → BN → ReLU 순서로 구성
nn.ReLU(inplace=True) :
• 비선형 활성화 함수 (Rectified Linear Unit)
• 입력이 0보다 작으면 0, 크면 그대로 출력 → 비선형성 부여
• inplace=True: 기존 메모리를 덮어써서 메모리 효율 개선 (주의해서 사용)
self.projection :
• Residual Block에서는 입력과 출력의 shape(크기)가 다를 수 있습니다.
• 이때 입력과 출력을 동일한 shape으로 맞춰주는 추가 연산 경로를 projection이라 합니다.
• 즉, skip connection의 "점선 버전"을 담당하는 역할입니다.
• self.projection(x)은 shortcut 경로로 사용되며, None일 경우 입력이 그대로 더해집니다.
ResNet의 핵심 구조는 Residual Learning이며, 그 중심에는 skip connection을 사용하는 Residual Block이 있다. 이 코드에서는 이를 BasicBlock과 Bottleneck이라는 두 클래스로 구분하여 구현하고 있다.
• 2개의 3x3 Conv + BatchNorm + ReLU로 구성된 블록
• expansion = 1로 설정되어 있어 입력과 출력 채널 수가 동일
• ResNet18, ResNet34 등 얕은 네트워크에서 사용됨
• projection 파라미터를 통해 stride ≠ 1이거나 채널 수가 다를 때 점선 연결을 허용
• 1x1 → 3x3 → 1x1 구조의 블록이며, 연산 효율성을 위해 설계됨
• expansion = 4로 설정되어 있어 출력 채널 수가 4배 증가
• ResNet50, ResNet101, ResNet152 등 깊은 네트워크에서 사용
• 1x1 Conv로 차원을 줄이고, 3x3으로 피처 추출 후, 다시 1x1로 복원하는 구조
• self.projection이 설정된 경우 점선 연결을 통해 shape mismatch 문제를 해결
• 모든 activation은 ReLU(inplace=True)로 설정되어 메모리 사용 최적화 적용
ResNet 클래스는 앞서 정의한 Block을 반복적으로 쌓아 전체 네트워크를 구성한다. 다양한 깊이의 ResNet 구조를 하나의 클래스에서 처리할 수 있도록 매우 유연하게 설계되었다.
• 7x7 Conv로 입력 이미지의 크기를 줄이고, 초기 피처를 추출
• 이어지는 3x3 max pooling으로 downsampling 수행
• 각 stage는 특정 블록(BasicBlock 또는 Bottleneck)을 여러 번 반복해서 구성
• stride=2를 통해 공간 해상도 감소(downsampling) 수행
• 첫 블록에서만 stride를 적용하며, projection이 필요할 경우 자동 적용됨
• 이후 블록들은 모두 stride=1, 실선 연결로 구성
self.avgpool → flatten → self.fc
• 전역 평균 풀링을 통해 feature map을 1x1로 줄임
• 마지막 Linear 레이어를 통해 클래스 수에 맞게 분류
ResNet 논문 및 후속 논문에서는 학습 초기 안정성이 중요한 이슈로 다뤄진다.
ReLU 비선형 함수와 함께 쓰이는 He 초기화를 모든 Conv에 적용
• 마지막 BatchNorm의 gamma 값을 0으로 초기화하여 블록이 초기에 identity 함수처럼 작동
• 학습 초기에 skip connection의 효과를 최대로 활용함
출처: https://arxiv.org/abs/1706.02677
코드 하단에 resnet18, resnet34, resnet50 등 다양한 ResNet 버전을 생성하는 팩토리 함수들이 존재한다.
• block type과 num_block_list만 바꿔서 각 버전의 네트워크 생성 가능
• kwargs를 통해 num_classes 등의 추가 인자를 유연하게 전달 가능
• main.py, train.py와 같은 외부 스크립트에서 호출하기에 최적화된 구조
Resnet 구현을 해보니 resnet의 구조를 가지고 조금 더 발전시켜볼 항목들이 있지 않을까라는 생각이 들었다. 다음 글에서는 이러한 점을 가지고 resnet의 구조를 조금 바꿔서 구현해보고자 한다. 물론 노트북으로 구현하기에 gpu의 한계가 있어 깊은 레이어를 다뤄보는데에는 한계가 있겠지만 resnet18을 사용해서 구현하고 성능을 비교해보려고 한다.