CH4_CNN & VGG16

s2ul3·2022년 10월 11일
0
  • 합성곱 : 작은 필터를 이용해 이미지로부터 특징을 뽑아내는 알고리즘
  • CNN : 합성곱층을 반복적으로 쌓아서 만든 인공 신경망
  • 커널 : 이미지 특징을 추출하기 위한 가중치 행렬.
  • 필터 : 커널들의 집합
  • 스트라이드(stride) : 커널의 이동거리
  • 특징맵 : 합성곱층의 결과. 즉 합성곱층이 특징을 추출한 뒤의 이미지
  • 패딩 : 이미지 외곽을 0으로 채우는 기법, 합성곱 전후 이미지 크기를 같게 만듦.
  • 크롭핑 : 이미지 일부분을 잘라내는 것
  • 최대 풀링 : 이미지 크기를 줄이는데 사용하는 기법으로 커널에서 가장 큰 값을 이용
  • 전이학습(pre training) : 사전 학습된 모델의 파라미터를 수정해 자신의 데이터셋에 최적화 시키는 방법. 학습에 걸리는 시간을 단축할 수 있음.

1. CNN 구조


대표적인 CNN모델 : VGG, ResNet, Inception

  • VGG : 가장 기본이 되는 CNN, 3*3크기의 커널을 이용해서 가중치 개수를 줄임.
    torchvision.models.vgg16()
  • ResNet : 입력 이미지와 특징맵을 더하는 CNN, 층이 깊어질수록 역전파되는 오차가 작아지는 문제를 해결. CNN모델 중에서 가장 많이 사용. 깊은 깊이.
    torchvision.models.resnet18()
  • Inception : 3*3 커널을 여러번 중첩해 크기가 큰 커널을 근사했다. VGG보다 넓은 시야를 갖으며, 큰 크기의 커널보다 적은 수의 가중치로 비슷한 효과를 얻음.
    torchvision.models.inception_v3()

2. 실습1

2-1. 데이터 전처리

# ❶ CIFAR10 데이터셋을 불러옴
training_data = CIFAR10(
    root="./",  # CIFAR-10 데이터를 내려받을 경로, "./" 는 현재 디렉토리를 의미함.
    train=True, # 학습용 데이터 : True // 평가용 데이터 : False
    download=True,  
    transform=ToTensor()) # 이미지를 파이토치 텐서로 변환.
    
 test_data = CIFAR10(
    root="./", 
    train=False, 
    download=True, 
    transform=ToTensor())
2-1-1. 데이터 증강
  • 오버피팅을 막고자 데이터를 의도적으로 수정해 더 많은 데이터를 확보하는 기법.
  • 회전, 크기변경, 밀림, 반사, 이동 등을 사용
  • 여기서는 크롭핑 후 잘라낸 부분을 0으로 채운 후 좌우대칭하여 데이터를 증강시키는 예시.
import matplotlib.pyplot as plt
import torchvision.transforms as T

from torchvision.datasets.cifar import CIFAR10
from torchvision.transforms import Compose
from torchvision.transforms import RandomHorizontalFlip, RandomCrop

transforms = Compose([ # ❶ 데이터 전처리 함수들, Compose 객체 안에 차례대로 넣어주면 순서대로 이미지에 적용됨.
   T.ToPILImage(),
   RandomCrop((32, 32), padding=4), # ➋ 랜덤으로 이미지 일부 제거 후 패딩. (32, 32)는 최종 출력 크기를 의미,
   RandomHorizontalFlip(p=0.5),     # ➌ y축을 기준으로 대칭, p는 이미지가 대칭될 확률을 의미
])

Compose([tf]) : 전처리 함수 tf를 입력받아 차례대로 실행
RandomCrop(size, padding = ) : padding크기만큼 0으로 패딩 후 size 크기만큼 이미지의 일부를 랜덤으로 뽑아냄.
RandomHorizontalFlip(p) : p확률로 이미지를 좌우대칭

2-2-2. 이미지 정규화
  • 데이터 분포를 정규분포 형태로 바꿔주는 것
  • 정규화 과정을 거치고나면 모든 색의 분포가 정규분포를 따르게 됨. --> 당연히 색도 바뀌게 되지만 신경쓰지말자. why? 인공지능 입장에서는 색의 분포가 일정해야 학습이 제대로 이뤄짐.
import matplotlib.pyplot as plt
import torchvision.transforms as T

from torchvision.datasets.cifar import CIFAR10
from torchvision.transforms import Compose
from torchvision.transforms import RandomHorizontalFlip, RandomCrop, Normalize

transforms = Compose([
   T.ToPILImage(),
   RandomCrop((32, 32), padding=4),
   RandomHorizontalFlip(p=0.5),
   T.ToTensor(),
   # ➊ 데이터 정규화
   Normalize(mean=(0.4914, 0.4822, 0.4465), std=(0.247, 0.243, 0.261)),
   T.ToPILImage()
])

training_data = CIFAR10(
    root="./", 
    train=True, 
    download=True, 
    transform=transforms)
test_data = CIFAR10(
    root="./", 
    train=False, 
    download=True, 
    transform=transforms)

Normalize(mean, std) : 평균 mean, 표준편차 std를 갖는 정규분포가 되도록 정규화
❓그렇다면 평균과 표준편차는 어떻게 구할까❓

import torch

training_data = CIFAR10(
    root="./", 
    train=True, 
    download=True, 
    transform=ToTensor())

# item[0]은 이미지, item[1]은 정답 레이블
imgs = [item[0] for item in training_data]

# imgs는 여러 이미지를 담고 있는 리스트. 파이토치에서 사용하려면 리스트를 텐서로 바꿔야함.
# --> 이미지가 여러개 담긴 '리스트' --> 이미지 여러개를 담는 '텐서'로 변환해야함.
# ❶imgs를 하나로 합침
imgs = torch.stack(imgs, dim=0).numpy()

# rgb 각각의 평균
mean_r = imgs[:,0,:,:].mean()
mean_g = imgs[:,1,:,:].mean()
mean_b = imgs[:,2,:,:].mean()
print(mean_r,mean_g,mean_b)

# rgb 각각의 표준편차
std_r = imgs[:,0,:,:].std()
std_g = imgs[:,1,:,:].std()
std_b = imgs[:,2,:,:].std()
print(std_r,std_g,std_b)

stack(tensor, dim) : tensor를 dim 방향으로 합쳐줌.
ex) (224, 224)크기의 텐서들을 dim = 0 방향으로 텐서 세개를 합치면 (3, 224, 224)모양의 텐서가 됨.

2-3. CNN으로 이미지 분류하기

  • 간단한 신경망 : nn.Sequential // 복잡한 신경망 : nn.Module
  • flatten 층 : MLP층의 입력으로 사용하도록 이미지를 1차원 벡터로 변환하는 층
2-3-1. 기본 블록 정의하기

MaxPool2d(kernel, stride) : 최대 풀링. kernel은 커널 크기, stride는 커널이 이동하는 거리
Conv2d(in, out, kernel, stride): 합성곱을 계산. in : 입력 채널 개수, out : 출력 채널 개수, kernel : 커널 크기

import torch
import torch.nn as nn


class BasicBlock(nn.Module): # ❶ 기본 블록을 정의합니다.
   # __init__ : 모듈의 초기화를 담당, __init__()안에 기본블록을 구성하는 모든 층을 정의함.
   def __init__(self, in_channels, out_channels, hidden_dim): 
       # ❷ nn.Module 클래스의 요소 상속
       super(BasicBlock, self).__init__()

       # ❸ 합성곱층 정의
       # in_channels : 입력 채널 개수 // out_channels : 출력 채널 개수  // hidden_dim : 은닉 채널 개수
       self.conv1 = nn.Conv2d(in_channels, hidden_dim,
                              kernel_size=3, padding=1)
       self.conv2 = nn.Conv2d(hidden_dim, out_channels, 
                              kernel_size=3, padding=1)
       self.relu = nn.ReLU()

       # stride는 커널의 이동 거리를 의미합니다.
       self.pool = nn.MaxPool2d(kernel_size=2, stride=2) # 이미지 크기가 절반으로 줄어듦.
  
   def forward(self, x): # ➍  기본블록의 순전파를 계산하는 함수.
       x = self.conv1(x)
       x = self.relu(x)
       x = self.conv2(x)
       x = self.relu(x)
       x = self.pool(x)
      
       return x
2-3-2. CNN 모델 정의하기
  • 기본 블럭(BasicBlock) 한번 거칠 때마다 이미지 크기는 절반으로 줄어듦.
    --> 아래 작성한 CNN모델에서는 기본블록을 3번 거치므로 이미지 크기는 32*32 --> 16*16 --> 8*8 --> 4*4 와 같이 줄어든다.
  • 마지막(세번째) 기본 블록을 거친 후 출력 채널 수(out_channels)는 256이 된다.
    --> 하나의 이미지를 flatten 하게되면 4*4*256 = 4096개의 값을 가진 배열이 된다. 이때 텐서 전체를 일렬로 풀어주면 배치끼리 정보가 뒤섞여 버림 --> 이를 방지하고자 start_dim 파라미터를 추가해준다. start_dim은 풀어주기 시작하는 차원을 의미.
    flatten(A, start_dim) : 텐서 A를 1차원으로 풀어준다. start_dim 파라미터를 이용해 몇번째 차원부터 풀어줄지 결정할 수 있다.
class CNN(nn.Module):
   def __init__(self, num_classes): # num_classes는 클래스의 개수를 의미합니다
       super(CNN, self).__init__()

       # ❶ 합성곱 기본 블록의 정의
       self.block1 = BasicBlock(in_channels=3, out_channels=32, hidden_dim=16) # 이미지는 3개의 채널(R, G, B)로 이루어졌기 때문에 처음 입력 채널은 3이다.
       self.block2 = BasicBlock(in_channels=32, out_channels=128, hidden_dim=64)
       self.block3 = BasicBlock(in_channels=128, out_channels=256, 
                                hidden_dim=128)

       # ❷ 분류기 정의
       self.fc1 = nn.Linear(in_features=4096, out_features=2048)
       self.fc2 = nn.Linear(in_features=2048, out_features=256)
       self.fc3 = nn.Linear(in_features=256, out_features=num_classes)


       # ❸ 분류기의 활성화 함수
       self.relu = nn.ReLU()

   def forward(self, x):
       x = self.block1(x)
       x = self.block2(x)
       x = self.block3(x)  # 출력 모양: (-1, 256, 4, 4) 
       x = torch.flatten(x, start_dim=1) # ➍ 2차원 특징맵을 1차원으로

       x = self.fc1(x)
       x = self.relu(x)
       x = self.fc2(x)
       x = self.relu(x)
       x = self.fc3(x)

       return x
2-3-3. 모델 학습하기
from torch.utils.data.dataloader import DataLoader

from torch.optim.adam import Adam
# 데이터 전처리
transforms = Compose([
   RandomCrop((32, 32), padding=4),  # ❶ 랜덤 크롭핑
   RandomHorizontalFlip(p=0.5),  # ❷ y축으로 뒤집기
   ToTensor(),  # ❸ 텐서로 변환
   # ❹ 이미지 정규화
   Normalize(mean=(0.4914, 0.4822, 0.4465), std=(0.247, 0.243, 0.261))
])
# ❶ 학습 데이터와 평가 데이터 불러오기
training_data = CIFAR10(root="./", train=True, download=True, transform=transforms)
test_data = CIFAR10(root="./", train=False, download=True, transform=transforms)


# ❷ 데이터로더 정의
train_loader = DataLoader(training_data, batch_size=32, shuffle=True)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)


# ❸ 학습을 진행할 프로세서 설정
device = "cuda" if torch.cuda.is_available() else "cpu"


# ➍ CNN 모델 정의
model = CNN(num_classes=10)

# ➎ 모델을 device로 보냄
model.to(device)

파이토치의 to(device) 메서드는 device로 데이터, 텐서, 모델을 보내주는 역할을 함. 모델의 device와 데이터의 device가 일치하지 않으면 오류가 발생하므로 유의하자!

# ❶ 학습률 정의
lr = 1e-3

# ❷ 최적화 기법 정의
optim = Adam(model.parameters(), lr=lr)

# 학습 루프 정의
for epoch in range(100):
   for data, label in train_loader:  # ➌ 데이터 호출
       optim.zero_grad()  # ➍ 기울기 초기화

       preds = model(data.to(device))  # ➎ 모델의 예측

       # ➏ 예측값을 이용해 손실을 계산하고 오차를 역전파한 뒤 최적화 진행.
       loss = nn.CrossEntropyLoss()(preds, label.to(device)) 
       loss.backward() 
       optim.step() 

   if epoch==0 or epoch%10==9:  # 10번마다 손실 출력
       print(f"epoch{epoch+1} loss:{loss.item()}")


# 모델 저장
torch.save(model.state_dict(), "CIFAR.pth")

optim.zero_grad() : 기울기 0으로 초기화. 초기화 해주지 않으면 기울기가 쌓여 무한대로 발산하기 때문에 반드시 초기화해야함.
CrossEntropyLoss(A, B) : A와 B의 크로스엔트로피 계산

2-3-4. 모델 성능 평가
# 모델 불러오기
model.load_state_dict(torch.load("CIFAR.pth", map_location=device))

num_corr = 0

with torch.no_grad():
   for data, label in test_loader:

       output = model(data.to(device))
       preds = output.data.max(1)[1]
       corr = preds.eq(label.to(device).data).sum().item()
       num_corr += corr

   print(f"Accuracy:{num_corr/len(test_data)}")

약 83%의 정확도

  • model.load_state_dict(torch.load("MNIST.pth", map_location=device)) : 모델파일을 불러온다.
    - map_location : 불러올 위치를 말하는데 기본적으로 CPU에 불러오지만 원하는 장치에 불러올 수 있다. GPU를 갖고 있다면 GPU에, 없다면 CPU에 불러옴.
  • no_grad() : 기울기 계산 X, 학습할 때는 가중치를 업데이트하는데 기울기가 필요하지만, 평가는 가중치를 바꿀 필요 없으므로 기울기 계산 X
  • max(1)[1] : 가장 높은 값을 갖는 위치 반환.
    - 모든 텐서의 차원은 배치, 클래스 순서.
    • max(0)은 배치에서 가장 높은 값을 , max(1)은 클래스 차원에서 가장 높은 값을 반환한다는 뜻.
      - max()는 최대 예측값, 최대 예측값의 인덱스를 묶어서 리스트로 반환하므로 max(1)[1]은 모든 배치에 대해 가장 높은 클래스값을 갖는 인덱스만을 가져온다.
  • eq() : 값이 같으면 1, 다르면 0을 반환.
    전체코드

3-4. 전이학습 모델 VGG로 분류하기

전이학습(Trnasfer learning) : 다른 데이터를 이용해 학습된 모델을 갖고 내가 가진 데이터에 최적화시키는 학습방법. 보통 대용량 데이터로 사전 학습된 모델을 소규모 데이터에 최적화시킨다.

VGG16

  • 3*3 합성곱과 최대 풀링 사용.
  • 유용한 곳 : 이미지 분류, 생성, 세그멘테이션 등 이미지의 특징을 추출할 때 사용됨.
  • ImageNet 데이터셋으로 사전학습된 VGG16 모델을 전이학습하여 CIFAR-10 데이터셋을 학습해보자.
  • VGG모델의 마지막 분류기만 나의 데이터에 맞게 수정하면 곧바로 사용 가능.
  • ImageNet은 클래스가 1천개 이므로 마지막 층 분류기는 출력값으로 1천 개의 값이 나온다. CIFAR-10은 클래스가 10개이므로 분류기를 수정해야한다.

    노란색 박스가 Fully connected layer인데 마지막 출력값 개수를 10개로 수정하면된다.

3-4-1. 사전 학습된 모델 불러오기

import torch
import torch.nn as nn

from torchvision.models.vgg import vgg16

device = "cuda" if torch.cuda.is_available() else "cpu"

model = vgg16(pretrained=True) # ❶ vgg16 모델 객체 생성
fc = nn.Sequential( # ❷ 분류층의 정의
       nn.Linear(512 * 7 * 7, 4096),
       nn.ReLU(),
       nn.Dropout(), #❷ 드롭아웃층 정의
       nn.Linear(4096, 4096),
       nn.ReLU(),
       nn.Dropout(),
       nn.Linear(4096, 10),
   )

model.classifier = fc # ➍ VGG의 classifier를 수정한 코드로 덮어씀
model.to(device)
  • ❷ : vgg의 Fully connected 층의 코드를 그대로 불러온 것. 여기서 마지막 출력값 개수를 10으로 바꿔준다.
  • ③ : Dropout(p) : p 확률로 뉴런 값을 의도적으로 0으로 바꾼다. 기본값은 0.5로 50% 확률로 가중치가 사라진다. 오버피팅을 방지하고자 사용

3-4-2. 모델 학습하기

데이터 전처리와 증강

import tqdm

from torchvision.datasets.cifar import CIFAR10
from torchvision.transforms import Compose, ToTensor, Resize
from torchvision.transforms import RandomHorizontalFlip, RandomCrop, Normalize
from torch.utils.data.dataloader import DataLoader

from torch.optim.adam import Adam

transforms = Compose([
   Resize(224),
   RandomCrop((224, 224), padding=4),
   RandomHorizontalFlip(p=0.5),
   ToTensor(),
   Normalize(mean=(0.4914, 0.4822, 0.4465), std=(0.247, 0.243, 0.261))
])

데이터로더 정의

training_data = CIFAR10(root="./", train=True, download=True, transform=transforms)
test_data = CIFAR10(root="./", train=False, download=True, transform=transforms)

train_loader = DataLoader(training_data, batch_size=32, shuffle=True)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)

학습루프 정의

lr = 1e-4
optim = Adam(model.parameters(), lr=lr)

for epoch in range(5):
   iterator = tqdm.tqdm(train_loader) # ➊ 학습 로그 출력
   for data, label in iterator:
       optim.zero_grad()

       preds = model(data.to(device)) # 모델의 예측값 출력

       loss = nn.CrossEntropyLoss()(preds, label.to(device))
       loss.backward()
       optim.step()
     
       # ❷ tqdm이 출력할 문자열 : 현재 epoch와 loss를 출력함.
       iterator.set_description(f"epoch:{epoch+1} loss:{loss.item()}")

torch.save(model.state_dict(), "CIFAR_pretrained.pth") # 모델 저장

tqdm(iterable) : 학습 로그를 출력하는 함수. iterable에 train_loader 변수를 인수로 넣어주고 반복문 안에 들어갈 객체로 넣어주면 반복문이 돌아가면서 iterable 객체에 대한 진행 상황을 프로그레스바를 이용하여 보여준다. 학습 도중에 손실을 확인하는 목적으로 사용됨.

3-4-3. 모델 성능 평가하기.

model.load_state_dict(torch.load("CIFAR_pretrained.pth", map_location=device))

num_corr = 0

with torch.no_grad():
   for data, label in test_loader:

       output = model(data.to(device))
       preds = output.data.max(1)[1]
       corr = preds.eq(label.to(device).data).sum().item()
       num_corr += corr

   print(f"Accuracy:{num_corr/len(test_data)}")

--> Accuracy : 0.9274
사전학습된 vgg16모델을 사용한 결과 정확도는 92%로 앞선 예제(CNN)에서 보인 정확도보다 훨씬 높음을 알 수 있다.
전체코드

profile
statistics & computer science

0개의 댓글