5. 이미지 처리 능력이 탁월한 CNN
5.3 ResNet으로 컬러 데이터셋에 적용하기
컬러 데이터셋은 흑백 이미지보다 복잡하기 때문에 학습을 더 많이 해야함.
흑백은 채널 수가 하나지만, 컬러는 RGB로 총 3개이며 PNG 형식의 경우 투명도도 포함해 총 4개의 채널을 가진다.
EPOCHS = 300
BATCH_SIZE = 128
# 데이터 불러오기
train_loader = torch.utils.data.DataLoader(
datasets.CIFAR10('./.data',
train = True,
download = True,
transform = transforms.Compose([
transforms.RandomCrop(32, padding = 4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))
])),
batch_size = BATCH_SIZE, shuffle = True
)
test_loader = torch.utils.data.DataLoader(
datasets.CIFAR10('./.data',
train = False,
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))
])),
batch_size = BATCH_SIZE, shuffle = True
)
무작정 인공 신경망을 여러 개 겹친다고 학습 성능이 무한히 좋아지는 것은 아니다.
여러 단계 신경망 거친다 -> 최초 입력 이미지 정보 소실됨.
ResNet은 이 문제를 해결하는 방안을 제시한다.
네트워크를 작은 블록인 Residual 블록으로 나누고, Residual 블록의 출력에 입력이었던 x를 더함으로써 모델을 훨씬 깊게 설계함. => 입력과 출력의 차이를 학습하는 것이 성능이 좋다는 가설.
Residual 블록을 BasicBlock이라는 새로운 파이토치 모듈로 정의해서 사용한다.
class BasicBlock(nn.Module):
def __init__(self, in_planes, planes, stride = 1):
super(BasicBlock, self).__init__()
# Conv2d(입력 채널 수 = in_planes, 출력 채널 수=planes)
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size = 3,
stride = stride, padding = 1, bias = False)
self.bn1 = nn.BatchNorm2d(planes) # 배치 정규화
self.conv2 = nn.Conv2d(planes, planes, kernel_size = 3,
stride = 1, padding = 1, bias = False)
self.bn2 = nn.BatchNorm2d(planes)
# 입력 채널인 in_planes 를 받아,
# self.bn2 계층의 출력 크기와 같은 planes 와 더해주는 shortcut 모듈 정의.
# Sequential은 여러 모듈을 하나의 모듈로 묶는 역할.
self.shortcut = nn.Sequential()
if stride != 1 or in_planes != planes:
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, planes,
kernel_size = 1, stride = stride, bias = False),
nn.BatchNorm2d(planes)
)
# 데이터 흐름
# 입력 x -> 컨볼루션 -> 배치 정규화 -> 활성화 함수
# -> 입력 x를 shortcut 거쳐 planes 로 크기 같게 한 거 더하기
# -> Relu 활성화 함수 통과 -> 최종 출력
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x)
out = F.relu(out)
return out
BatchNorm2d
: 배치 정규화를 수행하는 계층.
배치 정규화 : 학습률을 너무 높게 잡을 때 기울기가 소실되거나 발산하는 증상을 예방하여 학습 과정을 안정화하는 방법.
학습 중 각 계층에 들어가는 입력을 평균과 분산으로 정규화함으로써 학습을 효율적으로 만들어준다. => 자체적으로 정규화 수행하기 때문에 드롭아웃과 같은 효과를 낸다는 장점이 있음.
둘의 차이는 드롭아웃은 학습 중 데이터 일부를 배제하여 간접적으로 과적합을 막는 방식이라면, 배치 정규화는 신경망 내부 데이터에 직접 영향을 주는 방식이다.
💡 참고 링크 : Conv2d() 파라미터, 파이토치 공식 문서 Conv2d()
모델 만들기
- 이미지 받아 컨볼루션, 배치정규화 층 거치기
- 여러 BasicBlock 층 통과
- 평균 풀링, 신경망 거쳐 예측 출력
class ResNet(nn.Module):
def __init__(self, num_classes = 10):
super(ResNet, self).__init__()
# self._make_layer 함수에서 각 층을 만들 때 전층 채널 출력값 기록용
self.in_planes = 16 # layer1 입력 채널 개수 = 16
self.conv1 = nn.Conv2d(3, 16, kernel_size = 3,
stride = 1, padding = 1, bias = False)
self.bn1 = nn.BatchNorm2d(16)
self.layer1 = self._make_layer(16, 2, stride = 1) # 반환값 (16, 32, 32)
self.layer2 = self._make_layer(32, 2, stride = 2) # (32, 16, 16)
self.layer3 = self._make_layer(64, 2, stride = 2) # (64, 8, 8)
# 평균 풀링으로 텐서에 있는 원소 개수 64개 -> 10개(레이블 개수)
self.linear = nn.Linear(64, num_classes)
# 여러 BasicBlock을 모듈 하나로 묶어주는 역할.
# 반환값은 컨볼루션 계층과 마찬가지로 모듈 취급하면 된다.
def _make_layer(self, planes, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1)
layers = []
for stride in strides:
# BasicBlock 생성함.
# layer1 에서는 16 -> 16 인 BasicBlcok 2개
# layer2 에서는 16 -> 32 인 BasicBlock 1개, 32 -> 32 인 블록 1개
# layer3 에서는 32 -> 32 1개, 32 -> 64 1개
# 여기서 16 -> 32 와 32 -> 64 가 이저너 입력을 중간층에 더해 이미지 맥락 보존 역할.
layers.append(BasicBlock(self.in_planes, planes, stride))
self.in_planes = planes
return nn.Sequential(*layers)
# 데이터 흐름
# 1. 입력 들어오면 (컨볼루션, 배치정규화, 활성화함수) 통과
# 2. 정의해둔 BasicBlock 갖고 있는 layer1, 2, 3 통과
# 3. 나온 값에 평균 풀링 -> 마지막 계층 분류 결과 출력
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = F.avg_pool2d(out, 8)
out = out.view(out.size(0), -1)
out = self.linear(out)
return out
# 학습률 감소 기법 사용
# 학습 진행하면서 최적화 함수의 학습률을 점점 낮춰서 더 정교화게 최적화 함.
model = ResNet().to(DEVICE)
optimizer = optim.SGD(model.parameters(), lr = 0.1,
momentum = 0.9, weight_decay = 0.0005)
# 이폭마다 호출되며 step_size가 50이니, 50번 호출될 때 학습률에 gamma(=0.1)값을 곱한다.
# 0.1로 시작한 학습률은 50 이폭 후에 0.01이 됨.
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size = 50, gamma = 0.1)
print(model)
출력값
ResNet(
(conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
(1): BasicBlock(
(conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential(
(0): Conv2d(16, 32, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential(
(0): Conv2d(32, 64, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
)
(linear): Linear(in_features=64, out_features=10, bias=True)
)
# 학습 및 테스트
# 앞과 다른 점 : scheduler.step() 함수로 학습률 낮춰주는 단계 추가
def train(model, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(DEVICE), target.to(DEVICE)
optimizer.zero_grad()
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
optimizer.step()
def evaluate(model, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(DEVICE), target.to(DEVICE)
output = model(data)
test_loss += F.cross_entropy(output, target, reduction = "sum").item()
pred = output.max(1, keepdim = True)[1]
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
test_accuracy = 100. * correct / len(test_loader.dataset)
return test_loss, test_accuracy
for epoch in range(1, EPOCHS + 1):
scheduler.step()
train(model, train_loader, optimizer, epoch)
test_loss, test_accuracy = test(model, test_loader)
print(f"[{epoch}] Test Loss: {test_loss:.4f}, Accuracy: {test_accuracy:.2f}%")
여기까지 실행하니 에러가 났다…
💡 공식 문서 링크
Warning 자체는 optimizer.step
을 한 번도 호출하지 않은 상태에서 lr_scheduler.step
을 호출했기 때문이고, 그래서 lr scheduler step
을 optimizer step
보다 먼저 호출하게 작성해야 한다.
for epoch in range(1, EPOCHS + 1):
train(model, train_loader, optimizer, epoch)
scheduler.step()
...
으로 수정하여 실행했더니 잘 실행된다.
출력값
...
[72] Test Loss: 0.3341, Accuracy: 89.14%
[73] Test Loss: 0.3284, Accuracy: 89.51%
[74] Test Loss: 0.3363, Accuracy: 89.10%
정확도가 89%로 (책 기준 91%) 이전보다 성능이 떨어진다 생각할 수 있지만 CIFAR-10은 성능을 충분히 올리기엔 비교적 작은 데이터셋이기 때문에 이렇게 나온다고 한다.
💡 해당 포스팅은 펭귄브로의 3분 딥러닝, 파이토치맛 교재를 통해 학습한 내용을 정리한 글입니다.